use crate::error::OxenError;
use serde::{Deserialize, Serialize};
use utoipa::ToSchema;
use uuid::Uuid;
#[derive(Serialize, Deserialize, Debug, Clone, ToSchema)]
pub struct Remote {
pub name: String,
pub url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub repo_uuid: Option<Uuid>,
}
impl Remote {
pub fn new(name: &str, url: &str) -> Remote {
Remote {
name: name.to_string(),
url: url.to_string(),
repo_uuid: None,
}
}
pub(crate) fn with_repo_uuid_if_absent(self, repo_uuid: Option<Uuid>) -> Remote {
Remote {
repo_uuid: self.repo_uuid.or(repo_uuid),
..self
}
}
pub(crate) fn ensure_same_repo_uuid(&self, reported: Option<Uuid>) -> Result<(), OxenError> {
match (self.repo_uuid, reported) {
(Some(recorded), Some(reported)) if recorded != reported => {
Err(OxenError::RemotePointsAtDifferentRepo {
name: self.name.clone(),
url: self.url.clone(),
recorded,
reported,
})
}
_ => Ok(()),
}
}
}
impl std::fmt::Display for Remote {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "[{}] '{}'", self.name, self.url)
}
}
impl std::error::Error for Remote {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_remote_without_a_uuid_loads_with_none() {
let remote: Remote =
toml::from_str("name = \"origin\"\nurl = \"http://localhost:3000/ox/cats\"\n")
.expect("a remote predating the field parses");
assert_eq!(remote.repo_uuid, None);
}
#[test]
fn an_absent_uuid_is_not_written_at_all() {
let toml = toml::to_string(&Remote::new("origin", "http://localhost:3000/ox/cats"))
.expect("serialize");
assert!(!toml.contains("repo_uuid"), "unexpected key in:\n{toml}");
}
#[test]
fn a_disagreeing_reported_uuid_is_refused() {
let remote = Remote::new("origin", "http://localhost:3000/ox/cats")
.with_repo_uuid_if_absent(Some(Uuid::new_v4()));
let result = remote.ensure_same_repo_uuid(Some(Uuid::new_v4()));
assert!(
matches!(result, Err(OxenError::RemotePointsAtDifferentRepo { .. })),
"expected a refusal, got: {result:?}"
);
}
#[test]
fn an_agreeing_or_absent_uuid_is_accepted() {
let repo_uuid = Uuid::new_v4();
let recorded = Remote::new("origin", "http://localhost:3000/ox/cats")
.with_repo_uuid_if_absent(Some(repo_uuid));
let unrecorded = Remote::new("origin", "http://localhost:3000/ox/cats");
assert!(recorded.ensure_same_repo_uuid(Some(repo_uuid)).is_ok());
assert!(recorded.ensure_same_repo_uuid(None).is_ok());
assert!(unrecorded.ensure_same_repo_uuid(Some(repo_uuid)).is_ok());
assert!(unrecorded.ensure_same_repo_uuid(None).is_ok());
}
#[test]
fn a_recorded_uuid_round_trips() {
let repo_uuid = Uuid::new_v4();
let remote = Remote::new("origin", "http://localhost:3000/ox/cats")
.with_repo_uuid_if_absent(Some(repo_uuid));
let loaded: Remote =
toml::from_str(&toml::to_string(&remote).expect("serialize")).expect("deserialize");
assert_eq!(loaded.repo_uuid, Some(repo_uuid));
}
}