substrait/parse/proto/
version.rs1use crate::{proto, version};
6use hex::FromHex;
7use thiserror::Error;
8
9#[derive(Clone, Debug, PartialEq)]
13pub struct Version {
14 version: semver::Version,
16 git_hash: Option<[u8; 20]>,
18 producer: Option<String>,
20}
21
22impl Version {
23 pub fn version(&self) -> &semver::Version {
28 &self.version
29 }
30
31 pub fn git_hash(&self) -> Option<&[u8; 20]> {
35 self.git_hash.as_ref()
36 }
37
38 pub fn producer(&self) -> Option<&str> {
42 self.producer.as_deref()
43 }
44
45 pub(crate) fn compatible(&self) -> Result<(), VersionError> {
48 let version = self.version();
49 let version_req = version::semver_req();
50 version_req
51 .matches(version)
52 .then_some(())
53 .ok_or_else(|| VersionError::Substrait(version.clone(), version_req))
54 }
55}
56
57#[derive(Debug, Error, PartialEq)]
59pub enum VersionError {
60 #[error(
62 "git hash must be a lowercase hex ASCII string, 40 characters in length: (git hash: {0})"
63 )]
64 GitHash(String),
65
66 #[error("version must be specified")]
68 Missing,
69
70 #[error("substrait version incompatible (version: `{0}`, supported: `{1}`)")]
72 Substrait(semver::Version, semver::VersionReq),
73}
74
75impl TryFrom<proto::Version> for Version {
76 type Error = VersionError;
77
78 fn try_from(value: proto::Version) -> Result<Self, Self::Error> {
79 let proto::Version {
80 major_number,
81 minor_number,
82 patch_number,
83 git_hash,
84 producer,
85 } = value;
86
87 if major_number == u32::default()
90 && minor_number == u32::default()
91 && patch_number == u32::default()
92 {
93 return Err(VersionError::Missing);
94 }
95
96 if !git_hash.is_empty()
99 && (git_hash.len() != 40
100 || !git_hash.chars().all(|x| matches!(x, '0'..='9' | 'a'..='f')))
101 {
102 return Err(VersionError::GitHash(git_hash));
103 }
104
105 let version = Version {
106 version: semver::Version::new(major_number as _, minor_number as _, patch_number as _),
107 git_hash: (!git_hash.is_empty()).then(|| <[u8; 20]>::from_hex(git_hash).unwrap()),
108 producer: (!producer.is_empty()).then_some(producer),
109 };
110
111 version.compatible()?;
113
114 Ok(version)
115 }
116}
117
118impl From<Version> for proto::Version {
119 fn from(version: Version) -> Self {
120 let Version {
121 version,
122 git_hash,
123 producer,
124 } = version;
125
126 proto::Version {
127 major_number: version.major as _,
130 minor_number: version.minor as _,
131 patch_number: version.patch as _,
132 git_hash: git_hash.map(hex::encode).unwrap_or_default(),
133 producer: producer.unwrap_or_default(),
134 }
135 }
136}
137
138#[cfg(test)]
139mod tests {
140 use super::*;
141
142 #[test]
143 fn version() -> Result<(), VersionError> {
144 let version = proto::Version::default();
145 assert_eq!(Version::try_from(version), Err(VersionError::Missing));
146
147 let version = version::version();
148 Version::try_from(version)?;
149 Ok(())
150 }
151
152 #[test]
153 fn git_hash() {
154 let base = version::version();
155
156 let git_hash = String::from("short");
158 let version = proto::Version {
159 git_hash: git_hash.clone(),
160 ..base.clone()
161 };
162 assert_eq!(
163 Version::try_from(version),
164 Err(VersionError::GitHash(git_hash))
165 );
166
167 let git_hash = String::from("2FD4E1C67A2D28FCED849EE1BB76E7391B93EB12");
169 let version = proto::Version {
170 git_hash: git_hash.clone(),
171 ..base.clone()
172 };
173 assert_eq!(
174 Version::try_from(version),
175 Err(VersionError::GitHash(git_hash))
176 );
177
178 let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb1g");
180 let version = proto::Version {
181 git_hash: git_hash.clone(),
182 ..base.clone()
183 };
184 assert_eq!(
185 Version::try_from(version),
186 Err(VersionError::GitHash(git_hash))
187 );
188
189 let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb1å");
191 let version = proto::Version {
192 git_hash: git_hash.clone(),
193 ..base.clone()
194 };
195 assert_eq!(
196 Version::try_from(version),
197 Err(VersionError::GitHash(git_hash))
198 );
199
200 let git_hash = String::from("2fd4e1c67a2d28fced849ee1bb76e7391b93eb12");
202 let version = proto::Version { git_hash, ..base };
203 assert!(Version::try_from(version).is_ok());
204 }
205
206 #[test]
207 fn producer() -> Result<(), VersionError> {
208 let version = proto::Version {
210 producer: String::from(""),
211 ..version::version()
212 };
213 let version = Version::try_from(version)?;
214 assert!(version.producer.is_none());
215 Ok(())
216 }
217
218 #[test]
219 fn convert() -> Result<(), VersionError> {
220 let version = version::version();
221 assert_eq!(
222 proto::Version::from(Version::try_from(version.clone())?),
223 version
224 );
225 Ok(())
226 }
227
228 #[test]
229 fn compatible() -> Result<(), VersionError> {
230 let _version = Version::try_from(version::version())?;
231
232 let mut version = version::version();
233 version.major_number += 1;
234 let version = Version::try_from(version);
235 matches!(version, Err(VersionError::Substrait(_, _)));
236
237 let mut version = version::version();
238 version.minor_number += 1;
239 let version = Version::try_from(version);
240 matches!(version, Err(VersionError::Substrait(_, _)));
241
242 let mut version = version::version();
243 version.patch_number += 1;
244 let _version = Version::try_from(version)?;
245
246 Ok(())
247 }
248}