use std::collections::{BTreeMap, BTreeSet, VecDeque};
use std::sync::Mutex;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Changed {
Nothing,
Some(BTreeSet<String>),
Everything,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
pub struct What(u8);
impl What {
pub const NONE: What = What(0);
pub const CITATION: What = What(1);
pub const BODY: What = What(2);
pub const LINKS_OUT: What = What(4);
pub const CREATED: What = What(8);
pub const REMOVED: What = What(16);
pub const IDENTITY: What = What(32);
pub const BROAD: What = What(1 | 2 | 4);
pub const EVERYTHING: What = What(1 | 2 | 4 | 8 | 16 | 32);
pub const fn contains(self, other: What) -> bool {
self.0 & other.0 == other.0
}
pub const fn intersects(self, other: What) -> bool {
self.0 & other.0 != 0
}
pub const fn is_empty(self) -> bool {
self.0 == 0
}
pub const fn union(self, other: What) -> What {
What(self.0 | other.0)
}
pub const fn without(self, other: What) -> What {
What(self.0 & !other.0)
}
pub fn of_edit(path: &str) -> What {
let in_citation = path == "Citation" || path.starts_with("Citation.");
let mut what = if in_citation {
What::CITATION
} else {
What::BODY.union(What::LINKS_OUT)
};
let last = path.rsplit('.').next().unwrap_or(path);
if last.eq_ignore_ascii_case("uuid")
|| last.eq_ignore_ascii_case("uid")
|| last.eq_ignore_ascii_case("objectVersion")
|| last.eq_ignore_ascii_case("schemaVersion")
{
what = what.union(What::BROAD).union(What::IDENTITY);
}
what
}
}
impl std::ops::BitOr for What {
type Output = What;
fn bitor(self, other: What) -> What {
self.union(other)
}
}
impl std::ops::BitOrAssign for What {
fn bitor_assign(&mut self, other: What) {
*self = self.union(other);
}
}
impl std::fmt::Debug for What {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let names = [
(What::CITATION, "CITATION"),
(What::BODY, "BODY"),
(What::LINKS_OUT, "LINKS_OUT"),
(What::CREATED, "CREATED"),
(What::REMOVED, "REMOVED"),
(What::IDENTITY, "IDENTITY"),
];
let set: Vec<&str> = names
.iter()
.filter(|(flag, _)| self.contains(*flag))
.map(|(_, name)| *name)
.collect();
if set.is_empty() {
write!(f, "NONE")
} else {
write!(f, "{}", set.join(" | "))
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ObjectChange {
pub uuid: String,
pub uri: Option<String>,
pub what: What,
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct DatasetId {
pub file: String,
pub path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Change {
pub from: u64,
pub to: u64,
pub objects: Vec<ObjectChange>,
pub datasets: Vec<DatasetId>,
pub files: bool,
pub renamed: Vec<(String, String)>,
}
impl Change {
pub fn is_empty(&self) -> bool {
self.objects.is_empty() && self.datasets.is_empty() && !self.files && self.renamed.is_empty()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Since {
Exact(Change),
Everything,
}
#[derive(Debug, Clone)]
enum Entry {
Object {
uuid: String,
uri: Option<String>,
what: What,
},
Datasets {
file: String,
paths: Vec<String>,
},
Files,
Renamed {
proxy: String,
file: String,
},
}
#[derive(Debug, Clone)]
struct Inner {
current: u64,
log: VecDeque<(u64, Entry)>,
floor: u64,
}
#[derive(Debug)]
pub struct Revisions {
inner: Mutex<Inner>,
capacity: usize,
}
impl Clone for Revisions {
fn clone(&self) -> Self {
Self {
inner: Mutex::new(self.lock().clone()),
capacity: self.capacity,
}
}
}
impl Revisions {
pub fn new(capacity: usize) -> Self {
Self {
inner: Mutex::new(Inner {
current: 0,
log: VecDeque::new(),
floor: 0,
}),
capacity,
}
}
fn lock(&self) -> std::sync::MutexGuard<'_, Inner> {
self.inner.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
}
pub fn current(&self) -> u64 {
self.lock().current
}
fn push(&self, entry: Entry) -> u64 {
let mut inner = self.lock();
inner.current += 1;
let revision = inner.current;
if let Entry::Datasets { file, paths } = &entry
&& let Some((
last_revision,
Entry::Datasets {
file: last_file,
paths: last_paths,
},
)) = inner.log.back_mut()
&& last_file == file
{
last_paths.extend(paths.iter().cloned());
*last_revision = revision;
return revision;
}
inner.log.push_back((revision, entry));
while inner.log.len() > self.capacity {
if let Some((dropped, _)) = inner.log.pop_front() {
inner.floor = dropped;
}
}
revision
}
pub fn touch(&mut self, uuid: &str) -> u64 {
self.touch_what(uuid, None, What::BROAD)
}
pub fn touch_what(&self, uuid: &str, uri: Option<&str>, what: What) -> u64 {
self.push(Entry::Object {
uuid: uuid.to_ascii_lowercase(),
uri: uri.map(str::to_string),
what,
})
}
pub fn note_datasets<'a>(&self, file: &str, paths: impl IntoIterator<Item = &'a str>) -> u64 {
let mut paths: Vec<String> = paths.into_iter().map(str::to_string).collect();
if paths.is_empty() {
paths.push(String::new());
}
self.push(Entry::Datasets {
file: file.to_string(),
paths,
})
}
pub fn note_files(&self) -> u64 {
self.push(Entry::Files)
}
pub fn note_renamed(&self, proxy: &str, file: &str) -> u64 {
self.push(Entry::Renamed {
proxy: proxy.to_ascii_lowercase(),
file: file.to_string(),
})
}
pub fn touch_all(&mut self) -> u64 {
let mut inner = self.lock();
inner.current += 1;
inner.log.clear();
inner.floor = inner.current;
inner.current
}
pub fn narrow(&self, revision: u64, uuid: &str, flags: What) {
let uuid = uuid.to_ascii_lowercase();
let mut inner = self.lock();
for (at, entry) in inner.log.iter_mut() {
if *at == revision
&& let Entry::Object { uuid: held, what, .. } = entry
&& *held == uuid
{
*what = what.without(flags);
}
}
}
pub fn changed_since(&self, since: u64) -> Changed {
let inner = self.lock();
if since == inner.current {
return Changed::Nothing;
}
if since > inner.current || since < inner.floor {
return Changed::Everything;
}
Changed::Some(
inner
.log
.iter()
.filter(|(revision, _)| *revision > since)
.filter_map(|(_, entry)| match entry {
Entry::Object { uuid, .. } => Some(uuid.clone()),
_ => None,
})
.collect(),
)
}
pub fn changes_since(&self, since: u64) -> Since {
let inner = self.lock();
if since > inner.current || since < inner.floor {
return Since::Everything;
}
let mut change = Change {
from: since,
to: inner.current,
..Default::default()
};
let mut objects: BTreeMap<String, (Option<String>, What)> = BTreeMap::new();
let mut datasets: BTreeSet<DatasetId> = BTreeSet::new();
for entry in inner
.log
.iter()
.filter(|(revision, _)| *revision > since)
.map(|(_, entry)| entry)
{
match entry {
Entry::Object { uuid, uri, what } => {
let held = objects.entry(uuid.clone()).or_insert((None, What::NONE));
if uri.is_some() {
held.0 = uri.clone();
}
held.1 = merged(held.1, *what);
}
Entry::Datasets { file, paths } => {
for path in paths {
datasets.insert(DatasetId {
file: file.clone(),
path: path.clone(),
});
}
}
Entry::Files => change.files = true,
Entry::Renamed { proxy, file } => change.renamed.push((proxy.clone(), file.clone())),
}
}
change.objects = objects
.into_iter()
.map(|(uuid, (uri, what))| ObjectChange { uuid, uri, what })
.collect();
change.datasets = datasets.into_iter().collect();
Since::Exact(change)
}
}
fn merged(held: What, next: What) -> What {
let both = next.contains(What::CREATED) && next.contains(What::REMOVED);
if both {
return held.union(next);
}
if next.contains(What::REMOVED) {
return What::REMOVED;
}
if held.contains(What::REMOVED) {
return next.union(What::BROAD).union(What::CREATED);
}
held.union(next)
}
impl Default for Revisions {
fn default() -> Self {
Self::new(4096)
}
}