use std::collections::BTreeMap;
use std::path::PathBuf;
use multihash::Multihash;
use serde::Deserialize;
use serde::Serialize;
use serde::de;
use serde::ser;
use crate::Error;
use crate::Res;
use crate::error::LineageError;
use crate::lineage::status::UpstreamState;
use quilt_uri::ManifestUri;
fn multihash_to_str<S: ser::Serializer>(
hash: &Multihash<256>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let s = hex::encode(hash.to_bytes());
serializer.serialize_str(&s)
}
fn str_to_multihash<'de, D: de::Deserializer<'de>>(
deserializer: D,
) -> Result<Multihash<256>, D::Error> {
let s = String::deserialize(deserializer)?;
let bytes = hex::decode(s).map_err(de::Error::custom)?;
Multihash::from_bytes(&bytes).map_err(de::Error::custom)
}
#[derive(Default, Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct PathState {
pub timestamp: chrono::DateTime<chrono::Utc>,
#[serde(
serialize_with = "multihash_to_str",
deserialize_with = "str_to_multihash"
)]
pub hash: Multihash<256>,
}
pub type LineagePaths = BTreeMap<PathBuf, PathState>;
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, Default)]
pub struct CommitState {
pub timestamp: chrono::DateTime<chrono::Utc>,
pub hash: String,
#[serde(default = "Vec::new")]
pub prev_hashes: Vec<String>,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Default)]
pub struct PackageLineage {
pub commit: Option<CommitState>,
#[serde(default, rename = "remote", skip_serializing_if = "Option::is_none")]
pub remote_uri: Option<ManifestUri>,
pub base_hash: String,
pub latest_hash: String,
#[serde(default = "BTreeMap::new")]
pub paths: LineagePaths,
}
impl From<PackageLineage> for UpstreamState {
fn from(lineage: PackageLineage) -> Self {
if lineage.remote_uri.is_none()
|| lineage
.remote_uri
.as_ref()
.is_some_and(|r| r.hash.is_empty())
{
return Self::Local;
}
let behind = lineage.base_hash != lineage.latest_hash;
let ahead = lineage.base_hash != lineage.current_hash().unwrap_or_default();
match (ahead, behind) {
(false, false) => Self::UpToDate,
(false, true) => Self::Behind,
(true, false) => Self::Ahead,
(true, true) => Self::Diverged,
}
}
}
impl PackageLineage {
pub fn remote(&self) -> Res<&ManifestUri> {
self.remote_uri
.as_ref()
.ok_or(Error::Lineage(LineageError::NoRemote))
}
pub fn remote_mut(&mut self) -> Res<&mut ManifestUri> {
self.remote_uri
.as_mut()
.ok_or(Error::Lineage(LineageError::NoRemote))
}
pub fn from_remote(remote: ManifestUri, latest_hash: String) -> Self {
Self {
base_hash: remote.hash.clone(),
remote_uri: Some(remote),
latest_hash,
commit: None,
paths: BTreeMap::new(),
}
}
pub fn current_hash(&self) -> Option<&str> {
self.commit
.as_ref()
.map(|c| c.hash.as_str())
.or(self.remote_uri.as_ref().map(|r| r.hash.as_str()))
.or(if self.base_hash.is_empty() {
None
} else {
Some(self.base_hash.as_str())
})
}
pub fn update_latest(&mut self, manifest_uri: ManifestUri) {
let new_latest_hash = manifest_uri.hash;
self.latest_hash.clone_from(&new_latest_hash);
self.base_hash.clone_from(&new_latest_hash);
}
}
impl From<ManifestUri> for PackageLineage {
fn from(uri: ManifestUri) -> Self {
Self {
base_hash: uri.hash.clone(),
remote_uri: Some(uri.clone()),
latest_hash: uri.hash.clone(),
commit: None,
paths: BTreeMap::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_default_is_local() {
assert_eq!(
UpstreamState::from(PackageLineage::default()),
UpstreamState::Local
);
}
#[test]
fn test_remote_configured_but_never_pushed_is_local() {
let lineage = PackageLineage {
remote_uri: Some(ManifestUri {
hash: String::new(),
bucket: "test-bucket".to_string(),
namespace: ("foo", "bar").into(),
..ManifestUri::default()
}),
..PackageLineage::default()
};
assert_eq!(UpstreamState::from(lineage), UpstreamState::Local);
}
#[test]
fn test_remote_returns_no_remote_error() {
let lineage = PackageLineage::default();
assert!(matches!(
lineage.remote(),
Err(Error::Lineage(LineageError::NoRemote))
));
}
#[test]
fn test_current_hash_without_remote() {
let lineage = PackageLineage::default();
assert_eq!(lineage.current_hash(), None);
}
}