diff options
Diffstat (limited to 'src/schema.rs')
-rw-r--r-- | src/schema.rs | 173 |
1 files changed, 173 insertions, 0 deletions
diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..ae8c00b --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,173 @@ +//! Database schema versions. + +use serde::Serialize; + +/// The type of schema version components. +pub type VersionComponent = u32; + +/// Schema version of the database storing the generation. +/// +/// An Obnam client can restore a generation using schema version +/// (x,y), if the client supports a schema version (x,z). If z < y, +/// the client knows it may not be able to the generation faithfully, +/// and should warn the user about this. If z >= y, the client knows +/// it can restore the generation faithfully. If the client does not +/// support any schema version x, it knows it can't restore the backup +/// at all. +#[derive(Debug, Clone, Copy, Serialize)] +pub struct SchemaVersion { + /// Major version. + pub major: VersionComponent, + /// Minor version. + pub minor: VersionComponent, +} + +impl SchemaVersion { + /// Create a new schema version object. + pub fn new(major: VersionComponent, minor: VersionComponent) -> Self { + Self { major, minor } + } + + /// Return the major and minor version number component of a schema version. + pub fn version(&self) -> (VersionComponent, VersionComponent) { + (self.major, self.minor) + } + + /// Is this schema version compatible with another schema version? + pub fn is_compatible_with(&self, other: &Self) -> bool { + self.major == other.major && self.minor >= other.minor + } +} + +impl std::fmt::Display for SchemaVersion { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}.{}", self.major, self.minor) + } +} + +impl std::str::FromStr for SchemaVersion { + type Err = SchemaVersionError; + fn from_str(s: &str) -> Result<Self, Self::Err> { + if let Some(pos) = s.find('.') { + let major = parse_int(&s[..pos])?; + let minor = parse_int(&s[pos + 1..])?; + Ok(SchemaVersion::new(major, minor)) + } else { + Err(SchemaVersionError::Invalid(s.to_string())) + } + } +} + +fn parse_int(s: &str) -> Result<VersionComponent, SchemaVersionError> { + if let Ok(i) = s.parse() { + Ok(i) + } else { + Err(SchemaVersionError::InvalidComponent(s.to_string())) + } +} + +/// Possible errors from parsing schema versions. +#[derive(Debug, thiserror::Error, PartialEq, Eq)] +pub enum SchemaVersionError { + /// Failed to parse a string as a schema version. + #[error("Invalid schema version {0:?}")] + Invalid(String), + + /// Failed to parse a string as a schema version component. + #[error("Invalid schema version component {0:?}")] + InvalidComponent(String), +} + +#[cfg(test)] +mod test { + use super::*; + use std::str::FromStr; + + #[test] + fn from_string() { + let v = SchemaVersion::from_str("1.2").unwrap(); + assert_eq!(v.version(), (1, 2)); + } + + #[test] + fn from_string_fails_if_empty() { + match SchemaVersion::from_str("") { + Err(SchemaVersionError::Invalid(s)) => assert_eq!(s, ""), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_empty_major() { + match SchemaVersion::from_str(".2") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, ""), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_empty_minor() { + match SchemaVersion::from_str("1.") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, ""), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_just_major() { + match SchemaVersion::from_str("1") { + Err(SchemaVersionError::Invalid(s)) => assert_eq!(s, "1"), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_nonnumeric_major() { + match SchemaVersion::from_str("a.2") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, "a"), + _ => unreachable!(), + } + } + + #[test] + fn from_string_fails_if_nonnumeric_minor() { + match SchemaVersion::from_str("1.a") { + Err(SchemaVersionError::InvalidComponent(s)) => assert_eq!(s, "a"), + _ => unreachable!(), + } + } + + #[test] + fn compatible_with_self() { + let v = SchemaVersion::new(1, 2); + assert!(v.is_compatible_with(&v)); + } + + #[test] + fn compatible_with_older_minor_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(1, 3); + assert!(new.is_compatible_with(&old)); + } + + #[test] + fn not_compatible_with_newer_minor_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(1, 3); + assert!(!old.is_compatible_with(&new)); + } + + #[test] + fn not_compatible_with_older_major_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(2, 0); + assert!(!new.is_compatible_with(&old)); + } + + #[test] + fn not_compatible_with_newer_major_version() { + let old = SchemaVersion::new(1, 2); + let new = SchemaVersion::new(2, 0); + assert!(!old.is_compatible_with(&new)); + } +} |