substrait/parse/proto/
version.rs1use crate::{
6    parse::{Parse, context::Context},
7    proto, version,
8};
9use hex::FromHex;
10use thiserror::Error;
11
12#[derive(Clone, Debug, PartialEq)]
16pub struct Version {
17    version: semver::Version,
19    git_hash: Option<[u8; 20]>,
21    producer: Option<String>,
23}
24
25impl Version {
26    pub fn version(&self) -> &semver::Version {
31        &self.version
32    }
33
34    pub fn git_hash(&self) -> Option<&[u8; 20]> {
38        self.git_hash.as_ref()
39    }
40
41    pub fn producer(&self) -> Option<&str> {
45        self.producer.as_deref()
46    }
47
48    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#[derive(Debug, Error, PartialEq)]
62pub enum VersionError {
63    #[error(
65        "git hash must be a lowercase hex ASCII string, 40 characters in length: (git hash: {0})"
66    )]
67    GitHash(String),
68
69    #[error("version must be specified")]
71    Missing,
72
73    #[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        if major_number == u32::default()
94            && minor_number == u32::default()
95            && patch_number == u32::default()
96        {
97            return Err(VersionError::Missing);
98        }
99
100        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        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            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        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        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        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        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        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        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}