use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MottoLock {
pub format_version: u8,
pub version: Version,
pub fingerprint: String,
pub protocol_byte: u8,
pub updated_at: String,
#[serde(default)]
pub history: Vec<LockHistoryEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Version {
pub major: u16,
pub minor: u8,
pub patch: u16,
}
impl Version {
pub fn new(major: u16, minor: u8, patch: u16) -> Self {
Self {
major,
minor,
patch,
}
}
}
impl std::fmt::Display for Version {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}.{}.{}", self.major, self.minor, self.patch)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LockHistoryEntry {
pub version: String,
pub fingerprint: String,
pub timestamp: String,
pub description: Option<String>,
}
impl MottoLock {
pub fn new() -> Self {
Self {
format_version: 1,
version: Version::new(0, 1, 0),
fingerprint: String::new(),
protocol_byte: 1,
updated_at: chrono::Utc::now().to_rfc3339(),
history: Vec::new(),
}
}
pub fn parse_str(content: &str) -> Result<Self> {
if content.trim().starts_with('{') {
serde_json::from_str(content).context("Failed to parse motto.lock as JSON")
} else {
toml::from_str(content).context("Failed to parse motto.lock as TOML")
}
}
pub fn to_string(&self) -> Result<String> {
toml::to_string_pretty(self).context("Failed to serialize motto.lock")
}
pub fn version(&self) -> String {
self.version.to_string()
}
pub fn fingerprint(&self) -> &str {
&self.fingerprint
}
pub fn set_fingerprint(&mut self, fp: impl Into<String>) {
let new_fp = fp.into();
if !self.fingerprint.is_empty() && self.fingerprint != new_fp {
self.history.push(LockHistoryEntry {
version: self.version.to_string(),
fingerprint: self.fingerprint.clone(),
timestamp: self.updated_at.clone(),
description: None,
});
}
self.fingerprint = new_fp;
self.updated_at = chrono::Utc::now().to_rfc3339();
}
pub fn bump_major(&mut self) {
self.version.major += 1;
self.version.minor = 0;
self.version.patch = 0;
self.update_protocol_byte();
}
pub fn bump_minor(&mut self) {
self.version.minor = self.version.minor.wrapping_add(1);
self.version.patch = 0;
self.update_protocol_byte();
}
pub fn bump_patch(&mut self) {
self.version.patch += 1;
}
fn update_protocol_byte(&mut self) {
self.protocol_byte = self.version.minor;
}
}
impl Default for MottoLock {
fn default() -> Self {
Self::new()
}
}
use chrono;
use toml;
#[cfg(test)]
mod tests {
use super::*;
use pretty_assertions::assert_eq;
#[test]
fn test_lock_roundtrip() {
let mut lock = MottoLock::new();
lock.set_fingerprint("abc123def456");
lock.bump_minor();
let serialized = lock.to_string().unwrap();
let deserialized = MottoLock::parse_str(&serialized).unwrap();
assert_eq!(deserialized.version.major, 0);
assert_eq!(deserialized.version.minor, 2); assert_eq!(deserialized.fingerprint, "abc123def456");
}
#[test]
fn test_version_bumping() {
let mut lock = MottoLock::new();
assert_eq!(lock.version.to_string(), "0.1.0");
lock.bump_patch();
assert_eq!(lock.version.to_string(), "0.1.1");
lock.bump_minor();
assert_eq!(lock.version.to_string(), "0.2.0");
assert_eq!(lock.protocol_byte, 2);
lock.bump_major();
assert_eq!(lock.version.to_string(), "1.0.0");
assert_eq!(lock.protocol_byte, 0);
}
}