use std::collections::BTreeMap;
use std::path::Path;
use semver::Version;
use serde::{Deserialize, Serialize};
use dre_protocol::PluginId;
pub const LOCK_FILE: &str = "dre.lock";
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Locked {
pub version: Version,
pub sha256: Checksums,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub from: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub provides: Vec<PluginId>,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Checksums(BTreeMap<String, String>);
const LEGACY: &str = "";
impl Checksums {
pub fn new() -> Checksums {
Checksums::default()
}
pub fn legacy_only(sha: &str) -> Checksums {
let mut c = Checksums::new();
c.0.insert(LEGACY.into(), sha.to_lowercase());
c
}
pub fn get(&self, platform: &str) -> Option<&str> {
self.0.get(platform).map(String::as_str)
}
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn contains(&self, sha: &str) -> bool {
self.0
.iter()
.any(|(p, s)| p != LEGACY && s.eq_ignore_ascii_case(sha))
}
pub fn legacy(&self) -> Option<&str> {
self.get(LEGACY)
}
pub fn insert(&mut self, platform: &str, sha: &str) {
self.0.remove(LEGACY);
self.0.insert(platform.into(), sha.to_lowercase());
}
pub fn merge(&mut self, other: &Checksums) {
for (p, s) in &other.0 {
if p != LEGACY {
self.0.entry(p.clone()).or_insert_with(|| s.clone());
}
}
}
}
impl Serialize for Checksums {
fn serialize<S: serde::Serializer>(&self, s: S) -> Result<S::Ok, S::Error> {
match self.legacy() {
Some(sha) if self.0.len() == 1 => s.serialize_str(sha),
_ => self.0.serialize(s),
}
}
}
impl<'de> Deserialize<'de> for Checksums {
fn deserialize<D: serde::Deserializer<'de>>(d: D) -> Result<Self, D::Error> {
#[derive(Deserialize)]
#[serde(untagged)]
enum Raw {
One(String),
Many(BTreeMap<String, String>),
}
Ok(match Raw::deserialize(d)? {
Raw::One(sha) => Checksums::legacy_only(&sha),
Raw::Many(m) => Checksums(m.into_iter().map(|(k, v)| (k, v.to_lowercase())).collect()),
})
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct LockedPackage {
pub git: String,
pub revision: String,
pub commit: String,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct Lock {
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub plugins: BTreeMap<String, Locked>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub packages: BTreeMap<String, LockedPackage>,
#[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
pub local: BTreeMap<String, String>,
}
impl Lock {
pub fn load(root: &Path) -> Result<Lock, String> {
let p = root.join(LOCK_FILE);
match std::fs::read_to_string(&p) {
Ok(t) => serde_yaml_ng::from_str(&t).map_err(|e| format!("{LOCK_FILE}: {e}")),
Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(Lock::default()),
Err(e) => Err(format!("{LOCK_FILE}: {e}")),
}
}
pub fn save(&self, root: &Path) -> Result<(), String> {
let body = serde_yaml_ng::to_string(self).map_err(|e| e.to_string())?;
let text = format!(
"# Generated by DRE: the exact plugin versions and package commits this project uses. Commit this file.\n{body}"
);
std::fs::write(root.join(LOCK_FILE), text).map_err(|e| format!("{LOCK_FILE}: {e}"))
}
pub fn get(&self, package: &str) -> Option<&Locked> {
self.plugins.get(package)
}
pub fn version(&self, package: &str) -> Option<Version> {
self.get(package).map(|l| l.version.clone())
}
}