substrait/parse/proto/
version.rs1use crate::{
6 parse::{context::Context, Parse},
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}