Skip to main content

substrait/parse/text/simple_extensions/
file.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use super::{CustomType, SimpleExtensions, SimpleExtensionsError};
4use crate::parse::Parse;
5use crate::parse::text::simple_extensions::extensions::TypeContext;
6use crate::text::simple_extensions::SimpleExtensions as RawExtensions;
7use crate::urn::Urn;
8use std::io::Read;
9
10/// A parsed and validated [`RawExtensions`]: a simple extensions file.
11///
12/// An [`ExtensionFile`] has a canonical [`Urn`] and a parsed set of
13/// [`SimpleExtensions`] data. It represents the extensions file as a whole.
14#[derive(Debug)]
15pub struct ExtensionFile {
16    /// The URN this extension was loaded from
17    pub(crate) urn: Urn,
18    /// The extension data containing types and eventually functions
19    pub(crate) extension: SimpleExtensions,
20}
21
22impl ExtensionFile {
23    /// Create a new, empty [`ExtensionFile`] with an empty set of [`SimpleExtensions`].
24    pub fn empty(urn: Urn) -> Self {
25        let extension = SimpleExtensions::default();
26        Self { urn, extension }
27    }
28
29    /// Create an [`ExtensionFile`] from raw simple extension data.
30    pub fn create(extensions: RawExtensions) -> Result<Self, SimpleExtensionsError> {
31        // Parse all types (may contain unresolved Extension(String) references)
32        let mut ctx = TypeContext::default();
33        let file = Parse::parse(extensions, &mut ctx)?;
34
35        // TODO: Use ctx.known/ctx.linked to validate unresolved references and cross-file links.
36
37        Ok(file)
38    }
39
40    /// Get a type by name
41    pub fn get_type(&self, name: &str) -> Option<&CustomType> {
42        self.extension.get_type(name)
43    }
44
45    /// Get an iterator over all types in this extension
46    pub fn types(&self) -> impl Iterator<Item = &CustomType> {
47        self.extension.types()
48    }
49
50    /// Returns the [`Urn`]` for this extension file.
51    pub fn urn(&self) -> &Urn {
52        &self.urn
53    }
54
55    /// Get a reference to the underlying [`SimpleExtensions`].
56    pub fn extension(&self) -> &SimpleExtensions {
57        &self.extension
58    }
59
60    /// Convert the parsed extension file back into the raw text representation
61    /// by value.
62    pub fn into_raw(self) -> RawExtensions {
63        self.into()
64    }
65
66    /// Convert the parsed extension file back into the raw text representation
67    /// by reference.
68    pub fn to_raw(&self) -> RawExtensions {
69        super::extensions::to_raw_extensions(self.urn.clone(), self.extension.clone())
70    }
71
72    /// Read an extension file from a reader.
73    /// - `reader`: any [`Read`] instance with the YAML content
74    ///
75    /// Returns a parsed and validated [`ExtensionFile`] or an error.
76    pub fn read<R: Read>(reader: R) -> Result<Self, SimpleExtensionsError> {
77        let raw: RawExtensions = serde_yaml::from_reader(reader)?;
78        Self::create(raw)
79    }
80
81    /// Read an extension file from a string slice.
82    pub fn read_from_str<S: AsRef<str>>(s: S) -> Result<Self, SimpleExtensionsError> {
83        let raw: RawExtensions = serde_yaml::from_str(s.as_ref())?;
84        Self::create(raw)
85    }
86}
87
88impl From<ExtensionFile> for RawExtensions {
89    fn from(file: ExtensionFile) -> Self {
90        let ExtensionFile { urn, extension } = file;
91        super::extensions::to_raw_extensions(urn, extension)
92    }
93}
94
95// Parsing and conversion implementations are defined on `SimpleExtensions` in `extensions.rs`.
96
97#[cfg(test)]
98mod tests {
99    use super::*;
100    use crate::parse::text::simple_extensions::types::ParameterConstraint as RawParameterType;
101
102    const YAML_PARAM_TEST: &str = r#"
103%YAML 1.2
104---
105urn: extension:example.com:param_test
106types:
107  - name: "ParamTest"
108    parameters:
109      - name: "K"
110        type: integer
111        min: 1
112        max: 10
113"#;
114
115    const YAML_UNRESOLVED_TYPE: &str = r#"
116%YAML 1.2
117---
118urn: extension:example.com:unresolved
119types:
120  - name: "Alias"
121    structure: List<Map<string, u!MissingType>>
122"#;
123
124    #[test]
125    fn yaml_round_trip_integer_param_bounds() {
126        let deserialized: RawExtensions = serde_yaml::from_str(YAML_PARAM_TEST).expect("parse ok");
127        let ext = ExtensionFile::create(deserialized.clone()).expect("create ok");
128        assert_eq!(ext.urn().to_string(), "extension:example.com:param_test");
129
130        let ty = ext.get_type("ParamTest").expect("type exists");
131        match &ty.parameters[..] {
132            [param] => match &param.param_type {
133                RawParameterType::Integer {
134                    min: actual_min,
135                    max: actual_max,
136                } => {
137                    assert_eq!(actual_min, &Some(1));
138                    assert_eq!(actual_max, &Some(10));
139                }
140                other => panic!("unexpected param type: {other:?}"),
141            },
142            other => panic!("unexpected parameters: {other:?}"),
143        }
144
145        let back = ext.to_raw();
146        assert_eq!(deserialized, back);
147    }
148
149    #[test]
150    fn unresolved_type_reference_errors() {
151        let err = ExtensionFile::read_from_str(YAML_UNRESOLVED_TYPE)
152            .expect_err("expected unresolved type reference error");
153
154        match err {
155            SimpleExtensionsError::UnresolvedTypeReference { type_name } => {
156                assert_eq!(type_name, "MissingType");
157            }
158            other => panic!("unexpected error type: {other:?}"),
159        }
160    }
161}