substrait/parse/proto/
version.rs

1// SPDX-License-Identifier: Apache-2.0
2
3//! Parsing of [proto::Version].
4
5use crate::{
6    parse::{context::Context, Parse},
7    proto, version,
8};
9use hex::FromHex;
10use thiserror::Error;
11
12/// A parsed [proto::Version].
13///
14/// This parses only for compatible versions. See [`version::semver_req`].
15#[derive(Clone, Debug, PartialEq)]
16pub struct Version {
17    /// The semantic version.
18    version: semver::Version,
19    /// The git hash if set as bytes.
20    git_hash: Option<[u8; 20]>,
21    /// The producer string if set.
22    producer: Option<String>,
23}
24
25impl Version {
26    /// Returns the semantic version of this version.
27    ///
28    /// See [proto::Version::major_number], [proto::Version::minor_number] and
29    /// [proto::Version::patch_number].
30    pub fn version(&self) -> &semver::Version {
31        &self.version
32    }
33
34    /// Returns the git hash of this version.
35    ///
36    /// See [proto::Version::git_hash].
37    pub fn git_hash(&self) -> Option<&[u8; 20]> {
38        self.git_hash.as_ref()
39    }
40
41    /// Returns the producer of this version.
42    ///
43    /// See [proto::Version::producer].
44    pub fn producer(&self) -> Option<&str> {
45        self.producer.as_deref()
46    }
47
48    /// Returns [VersionError::Substrait] if this version is incompatible with
49    /// the Substrait [version::version] of this crate.
50    pub(crate) fn compatible(&self) -> Result<(), VersionError> {
51        let version = self.version();
52        let version_req = version::semver_req();
53        version_req
54            .matches(version)
55            .then_some(())
56            .ok_or_else(|| VersionError::Substrait(version.clone(), version_req))
57    }
58}
59
60/// Parse errors for [proto::Version].
61#[derive(Debug, Error, PartialEq)]
62pub enum VersionError {
63    /// Git hash is incorrect.
64    #[error(
65        "git hash must be a lowercase hex ASCII string, 40 characters in length: (git hash: {0})"
66    )]
67    GitHash(String),
68
69    /// Version is missing.
70    #[error("version must be specified")]
71    Missing,
72
73    /// Version is incompatible.
74    #[error("substrait version incompatible (version: `{0}`, supported: `{1}`)")]
75    Substrait(semver::Version, semver::VersionReq),
76}
77
78impl<C: Context> Parse<C> for proto::Version {
79    type Parsed = Version;
80    type Error = VersionError;
81
82    fn parse(self, _ctx: &mut C) -> Result<Self::Parsed, Self::Error> {
83        let proto::Version {
84            major_number,
85            minor_number,
86            patch_number,
87            git_hash,
88            producer,
89        } = self;
90
91        // All version numbers unset (u32::default()) is an error, because
92        // version is required.
93        if major_number == u32::default()
94            && minor_number == u32::default()
95            && patch_number == u32::default()
96        {
97            return Err(VersionError::Missing);
98        }
99
100        // The git hash, when set, must be a lowercase hex ASCII string, 40
101        // characters in length.
102        if !git_hash.is_empty()
103            && (git_hash.len() != 40
104                || !git_hash.chars().all(|x| matches!(x, '0'..='9' | 'a'..='f')))
105        {
106            return Err(VersionError::GitHash(git_hash));
107        }
108
109        let version = Version {
110            version: semver::Version::new(major_number as _, minor_number as _, patch_number as _),
111            git_hash: (!git_hash.is_empty()).then(|| <[u8; 20]>::from_hex(git_hash).unwrap()),
112            producer: (!producer.is_empty()).then_some(producer),
113        };
114
115        // The version must be compatible with the substrait version of this crate.
116        version.compatible()?;
117
118        Ok(version)
119    }
120}
121
122impl From<Version> for proto::Version {
123    fn from(version: Version) -> Self {
124        let Version {
125            version,
126            git_hash,
127            producer,
128        } = version;
129
130        proto::Version {
131            // Note: we can use `as _` here because this Version is always
132            // constructed from `u32` values.
133            major_number: version.major as _,
134            minor_number: version.minor as _,
135            patch_number: version.patch as _,
136            git_hash: git_hash.map(hex::encode).unwrap_or_default(),
137            producer: producer.unwrap_or_default(),
138        }
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    use super::*;
145    use crate::parse::context::tests::Context;
146
147    #[test]
148    fn version() -> Result<(), VersionError> {
149        let version = proto::Version::default();
150        assert_eq!(
151            version.parse(&mut Context::default()),
152            Err(VersionError::Missing)
153        );
154
155        let version = version::version();
156        version.parse(&mut Context::default())?;
157        Ok(())
158    }
159
160    #[test]
161    fn git_hash() {
162        let base = version::version();
163
164        // Bad length.
165        let git_hash = String::from("short");
166        let version = proto::Version {
167            git_hash: git_hash.clone(),
168            ..base.clone()
169        };
170        assert_eq!(
171            version.parse(&mut Context::default()),
172            Err(VersionError::GitHash(git_hash))
173        );
174
175        // Not lowercase.
176        let git_hash = String::from("2FD4E1C67A2D28FCED849EE1BB76E7391B93EB12");
177        let version = proto::Version {
178            git_hash: git_hash.clone(),
179            ..base.clone()
180        };
181        assert_eq!(
182            version.parse(&mut Context::default()),
183            Err(VersionError::GitHash(git_hash))
184        );
185
186        // Not all hex digits.
187        let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb1g");
188        let version = proto::Version {
189            git_hash: git_hash.clone(),
190            ..base.clone()
191        };
192        assert_eq!(
193            version.parse(&mut Context::default()),
194            Err(VersionError::GitHash(git_hash))
195        );
196
197        // Not all ascii.
198        let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb1å");
199        let version = proto::Version {
200            git_hash: git_hash.clone(),
201            ..base.clone()
202        };
203        assert_eq!(
204            version.parse(&mut Context::default()),
205            Err(VersionError::GitHash(git_hash))
206        );
207
208        // Valid.
209        let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12");
210        let version = proto::Version { git_hash, ..base };
211        assert!(version.parse(&mut Context::default()).is_ok());
212    }
213
214    #[test]
215    fn producer() -> Result<(), VersionError> {
216        // Empty producer maps to `None`
217        let version = proto::Version {
218            producer: String::from(""),
219            ..version::version()
220        };
221        let version = version.parse(&mut Context::default())?;
222        assert!(version.producer.is_none());
223        Ok(())
224    }
225
226    #[test]
227    fn convert() -> Result<(), VersionError> {
228        let version = version::version();
229        assert_eq!(
230            proto::Version::from(version.clone().parse(&mut Context::default())?),
231            version
232        );
233        Ok(())
234    }
235
236    #[test]
237    fn compatible() -> Result<(), VersionError> {
238        let _version = version::version().parse(&mut Context::default())?;
239
240        let mut version = version::version();
241        version.major_number += 1;
242        let version = version.parse(&mut Context::default());
243        matches!(version, Err(VersionError::Substrait(_, _)));
244
245        let mut version = version::version();
246        version.minor_number += 1;
247        let version = version.parse(&mut Context::default());
248        matches!(version, Err(VersionError::Substrait(_, _)));
249
250        let mut version = version::version();
251        version.patch_number += 1;
252        let _version = version.parse(&mut Context::default())?;
253
254        Ok(())
255    }
256}