use std::collections::{BTreeMap, BTreeSet};
use energyml_core::registry::DynObject;
use crate::error::{Error, Result};
use super::revision::{Changed, Revisions, Since, What};
pub enum Change {
Put(Box<dyn DynObject>),
Removed,
}
impl Change {
fn clone_change(&self) -> Change {
match self {
Change::Put(object) => Change::Put(object.clone_box()),
Change::Removed => Change::Removed,
}
}
}
#[derive(Default)]
pub struct Overlay {
committed: BTreeMap<String, Change>,
transaction: Option<BTreeMap<String, Change>>,
raw: BTreeMap<String, Vec<u8>>,
raw_transaction: Option<BTreeMap<String, Vec<u8>>>,
superseded: BTreeSet<String>,
dropped: BTreeSet<String>,
media: BTreeMap<(String, String), bool>,
media_transaction: Option<BTreeMap<(String, String), bool>>,
revisions: Revisions,
files: u64,
}
impl Overlay {
pub fn revision(&self) -> u64 {
self.revisions.current()
}
pub fn files_revision(&self) -> u64 {
self.files
}
pub fn changed_since(&self, since: u64) -> Changed {
self.revisions.changed_since(since)
}
pub fn changes_since(&self, since: u64) -> Since {
self.revisions.changes_since(since)
}
pub fn journal(&self) -> &Revisions {
&self.revisions
}
pub fn touch_all(&mut self) {
self.revisions.touch_all();
}
pub fn new() -> Self {
Self::default()
}
pub fn is_dirty(&self) -> bool {
!self.committed.is_empty()
|| self.transaction.as_ref().is_some_and(|t| !t.is_empty())
|| !self.raw.is_empty()
|| self.raw_transaction.as_ref().is_some_and(|t| !t.is_empty())
|| !self.superseded.is_empty()
|| !self.dropped.is_empty()
|| !self.media.is_empty()
|| self.media_transaction.as_ref().is_some_and(|t| !t.is_empty())
}
pub fn set_media_link(&mut self, part: &str, uuid: &str, related: bool) {
self.files += 1;
self.revisions.note_files();
let key = (part.to_string(), uuid.to_ascii_lowercase());
match &mut self.media_transaction {
Some(transaction) => transaction.insert(key, related),
None => self.media.insert(key, related),
};
}
pub fn media_links(&self) -> Vec<((String, String), bool)> {
let mut out: BTreeMap<&(String, String), bool> = self.media.iter().map(|(k, v)| (k, *v)).collect();
if let Some(transaction) = &self.media_transaction {
out.extend(transaction.iter().map(|(k, v)| (k, *v)));
}
out.into_iter().map(|(k, v)| (k.clone(), v)).collect()
}
pub fn raw(&self, part: &str) -> Option<&[u8]> {
self.raw_transaction
.as_ref()
.and_then(|t| t.get(part))
.or_else(|| self.raw.get(part))
.map(Vec::as_slice)
}
pub fn set_raw(&mut self, part: &str, bytes: Vec<u8>) {
self.files += 1;
self.revisions.note_files();
match &mut self.raw_transaction {
Some(transaction) => transaction.insert(part.to_string(), bytes),
None => self.raw.insert(part.to_string(), bytes),
};
}
pub fn clear_raw(&mut self, part: &str) -> bool {
self.files += 1;
self.revisions.note_files();
match &mut self.raw_transaction {
Some(transaction) => transaction.remove(part).is_some(),
None => self.raw.remove(part).is_some(),
}
}
pub fn supersede(&mut self, part: &str) {
self.superseded.insert(part.to_string());
}
pub fn is_superseded(&self, part: &str) -> bool {
self.superseded.contains(part)
}
pub fn drop_part(&mut self, part: &str) {
self.files += 1;
self.revisions.note_files();
self.dropped.insert(part.to_string());
}
pub fn is_dropped(&self, part: &str) -> bool {
self.dropped.contains(part)
}
pub fn restore_part(&mut self, part: &str) -> bool {
self.files += 1;
self.revisions.note_files();
self.dropped.remove(part)
}
pub fn dropped_parts(&self) -> Vec<String> {
self.dropped.iter().cloned().collect()
}
pub fn raw_parts(&self) -> Vec<String> {
let mut out: BTreeMap<&String, ()> = self.raw.keys().map(|k| (k, ())).collect();
if let Some(transaction) = &self.raw_transaction {
out.extend(transaction.keys().map(|k| (k, ())));
}
out.into_keys().cloned().collect()
}
pub fn in_transaction(&self) -> bool {
self.transaction.is_some()
}
pub fn get(&self, uuid: &str) -> Option<&Change> {
let key = uuid.to_ascii_lowercase();
self.transaction
.as_ref()
.and_then(|t| t.get(&key))
.or_else(|| self.committed.get(&key))
}
pub fn get_mut(&mut self, uuid: &str) -> Option<&mut Box<dyn DynObject>> {
self.get_mut_what(uuid, None, What::BROAD)
}
pub fn get_mut_what(
&mut self,
uuid: &str,
uri: Option<&str>,
what: What,
) -> Option<&mut Box<dyn DynObject>> {
let key = uuid.to_ascii_lowercase();
let map = match &mut self.transaction {
Some(transaction) => transaction,
None => &mut self.committed,
};
match map.get_mut(&key) {
Some(Change::Put(object)) => {
self.revisions.touch_what(&key, uri, what);
Some(object)
}
_ => None,
}
}
pub fn get_mut_unjournaled(&mut self, uuid: &str) -> Option<&mut Box<dyn DynObject>> {
let key = uuid.to_ascii_lowercase();
let map = match &mut self.transaction {
Some(transaction) => transaction,
None => &mut self.committed,
};
match map.get_mut(&key) {
Some(Change::Put(object)) => Some(object),
_ => None,
}
}
pub fn set(&mut self, uuid: &str, change: Change) {
let what = match change {
Change::Removed => What::REMOVED,
Change::Put(_) => What::BROAD,
};
self.set_what(uuid, None, change, what);
}
pub fn set_what(&mut self, uuid: &str, uri: Option<&str>, change: Change, what: What) {
let key = uuid.to_ascii_lowercase();
self.revisions.touch_what(&key, uri, what);
self.place(key, change);
}
pub fn place_unjournaled(&mut self, uuid: &str, change: Change) {
self.place(uuid.to_ascii_lowercase(), change);
}
fn place(&mut self, key: String, change: Change) {
match &mut self.transaction {
Some(transaction) => transaction.insert(key, change),
None => self.committed.insert(key, change),
};
}
pub fn changed(&self) -> Vec<(&String, &Change)> {
let mut out: BTreeMap<&String, &Change> = self.committed.iter().collect();
if let Some(transaction) = &self.transaction {
out.extend(transaction.iter());
}
out.into_iter().collect()
}
pub fn start_transaction(&mut self) -> Result<()> {
if self.transaction.is_some() {
return Err(Error::Package(
"a transaction is already open; ETP v1.2 does not nest them".to_string(),
));
}
self.files += 1;
self.transaction = Some(BTreeMap::new());
self.raw_transaction = Some(BTreeMap::new());
self.media_transaction = Some(BTreeMap::new());
Ok(())
}
pub fn commit_transaction(&mut self) -> Result<()> {
let transaction = self.take_transaction()?;
self.files += 1;
self.committed.extend(transaction);
if let Some(raw) = self.raw_transaction.take() {
self.raw.extend(raw);
}
if let Some(media) = self.media_transaction.take() {
self.media.extend(media);
}
Ok(())
}
pub fn rollback_transaction(&mut self) -> Result<()> {
self.files += 1;
let outcome = self.take_transaction().map(|undone| {
for uuid in undone.keys() {
self.revisions.touch_what(uuid, None, What::EVERYTHING);
}
});
if self.media_transaction.as_ref().is_some_and(|t| !t.is_empty())
|| self.raw_transaction.as_ref().is_some_and(|t| !t.is_empty())
{
self.revisions.note_files();
}
if self.raw_transaction.as_ref().is_some_and(|t| !t.is_empty()) {
self.revisions.touch_all();
}
self.raw_transaction = None;
self.media_transaction = None;
outcome
}
fn take_transaction(&mut self) -> Result<BTreeMap<String, Change>> {
self.transaction
.take()
.ok_or_else(|| Error::Package("no transaction is open".to_string()))
}
pub fn clear(&mut self) {
self.files += 1;
self.committed.clear();
self.transaction = None;
self.raw.clear();
self.raw_transaction = None;
self.superseded.clear();
self.dropped.clear();
self.media.clear();
self.media_transaction = None;
}
}
impl Clone for Overlay {
fn clone(&self) -> Self {
let copy = |map: &BTreeMap<String, Change>| -> BTreeMap<String, Change> {
map.iter().map(|(k, v)| (k.clone(), v.clone_change())).collect()
};
Self {
committed: copy(&self.committed),
transaction: self.transaction.as_ref().map(copy),
raw: self.raw.clone(),
raw_transaction: self.raw_transaction.clone(),
superseded: self.superseded.clone(),
dropped: self.dropped.clone(),
media: self.media.clone(),
media_transaction: self.media_transaction.clone(),
revisions: self.revisions.clone(),
files: self.files,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SavePolicy {
#[default]
Manual,
OnChange,
OnClose,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_transaction_does_not_nest() {
let mut overlay = Overlay::new();
overlay.start_transaction().expect("the first one opens");
assert!(overlay.start_transaction().is_err(), "the second is refused");
overlay.commit_transaction().expect("it closes");
assert!(overlay.commit_transaction().is_err(), "and only once");
}
#[test]
fn a_rollback_actually_undoes() {
let mut overlay = Overlay::new();
overlay.set("A", Change::Removed);
overlay.start_transaction().unwrap();
overlay.set("B", Change::Removed);
assert_eq!(overlay.changed().len(), 2);
overlay.rollback_transaction().unwrap();
assert_eq!(overlay.changed().len(), 1, "B is gone, A stayed");
assert!(overlay.get("B").is_none());
assert!(overlay.get("a").is_some(), "uuids compare without case");
}
#[test]
fn a_commit_merges_downwards() {
let mut overlay = Overlay::new();
overlay.start_transaction().unwrap();
overlay.set("B", Change::Removed);
overlay.commit_transaction().unwrap();
assert!(!overlay.in_transaction());
assert!(overlay.get("B").is_some(), "it survived the commit");
}
}