use std::{
borrow::Cow,
collections::{HashMap, HashSet},
};
use anyhow::Result;
use crate::sdf::{Path, SpecData, SpecType, Value};
pub trait AbstractData {
fn has_spec(&self, path: &Path) -> bool;
fn has_field(&self, path: &Path, field: &str) -> bool;
fn spec_type(&self, path: &Path) -> Option<SpecType>;
fn try_field(&self, path: &Path, field: &str) -> Result<Option<Cow<'_, Value>>, DataError>;
fn get_field(&self, path: &Path, field: &str) -> Result<Cow<'_, Value>, DataError> {
self.try_field(path, field)?.ok_or_else(|| DataError::Missing {
path: path.clone(),
field: field.to_owned(),
})
}
fn list_fields(&self, path: &Path) -> Option<Vec<String>>;
fn spec_paths(&self) -> Vec<Path>;
fn create_spec(&mut self, path: Path, ty: SpecType);
fn erase_spec(&mut self, path: &Path);
fn set_field(&mut self, path: &Path, field: &str, value: Value);
fn erase_field(&mut self, path: &Path, field: &str);
}
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum DataError {
#[error("failed to decode field {field:?} at {path}")]
Decode {
path: Path,
field: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync + 'static>,
},
#[error("no field {field:?} at {path}")]
Missing {
path: Path,
field: String,
},
}
#[derive(Default, Debug, Clone)]
pub struct Data {
specs: HashMap<Path, SpecData>,
}
impl Data {
pub fn new() -> Self {
Self::default()
}
pub fn from_specs(specs: HashMap<Path, SpecData>) -> Self {
Self { specs }
}
pub fn from_abstract(src: &dyn AbstractData) -> Result<Self> {
let mut out = Self::new();
for path in src.spec_paths() {
let ty = src
.spec_type(&path)
.ok_or_else(|| anyhow::anyhow!("path {path} reported by paths() has no spec"))?;
let spec = out.create_spec(path.clone(), ty);
if let Some(fields) = src.list_fields(&path) {
for name in fields {
spec.add(&name, src.get_field(&path, &name)?.into_owned());
}
}
}
Ok(out)
}
pub fn create_spec(&mut self, path: Path, ty: SpecType) -> &mut SpecData {
self.specs.insert(path.clone(), SpecData::new(ty));
self.specs.get_mut(&path).expect("just inserted")
}
pub fn erase_spec(&mut self, path: &Path) -> Option<SpecData> {
self.specs.remove(path)
}
pub fn spec(&self, path: &Path) -> Option<&SpecData> {
self.specs.get(path)
}
pub fn spec_mut(&mut self, path: &Path) -> Option<&mut SpecData> {
self.specs.get_mut(path)
}
pub fn iter(&self) -> impl Iterator<Item = (&Path, &SpecData)> {
self.specs.iter()
}
pub fn len(&self) -> usize {
self.specs.len()
}
pub fn is_empty(&self) -> bool {
self.specs.is_empty()
}
}
impl AbstractData for Data {
fn has_spec(&self, path: &Path) -> bool {
self.specs.contains_key(path)
}
fn has_field(&self, path: &Path, field: &str) -> bool {
self.specs.get(path).is_some_and(|spec| spec.contains(field))
}
fn spec_type(&self, path: &Path) -> Option<SpecType> {
self.specs.get(path).map(|spec| spec.ty)
}
fn try_field(&self, path: &Path, field: &str) -> Result<Option<Cow<'_, Value>>, DataError> {
Ok(self.specs.get(path).and_then(|spec| spec.get(field)).map(Cow::Borrowed))
}
fn list_fields(&self, path: &Path) -> Option<Vec<String>> {
self.specs
.get(path)
.map(|spec| spec.fields.iter().map(|(k, _)| k.clone()).collect())
}
fn spec_paths(&self) -> Vec<Path> {
let mut paths: Vec<Path> = self.specs.keys().cloned().collect();
paths.sort_by(|a, b| a.as_str().cmp(b.as_str()));
paths
}
fn create_spec(&mut self, path: Path, ty: SpecType) {
self.specs.insert(path, SpecData::new(ty));
}
fn erase_spec(&mut self, path: &Path) {
self.specs.remove(path);
}
fn set_field(&mut self, path: &Path, field: &str, value: Value) {
match self.specs.get_mut(path) {
Some(spec) => spec.add(field, value),
None => debug_assert!(false, "set_field on absent spec at {path}"),
}
}
fn erase_field(&mut self, path: &Path, field: &str) {
if let Some(spec) = self.specs.get_mut(path) {
spec.remove(field);
}
}
}
impl<T: AbstractData + ?Sized> AbstractData for Box<T> {
fn has_spec(&self, path: &Path) -> bool {
(**self).has_spec(path)
}
fn has_field(&self, path: &Path, field: &str) -> bool {
(**self).has_field(path, field)
}
fn spec_type(&self, path: &Path) -> Option<SpecType> {
(**self).spec_type(path)
}
fn try_field(&self, path: &Path, field: &str) -> Result<Option<Cow<'_, Value>>, DataError> {
(**self).try_field(path, field)
}
fn list_fields(&self, path: &Path) -> Option<Vec<String>> {
(**self).list_fields(path)
}
fn spec_paths(&self) -> Vec<Path> {
(**self).spec_paths()
}
fn create_spec(&mut self, path: Path, ty: SpecType) {
(**self).create_spec(path, ty)
}
fn erase_spec(&mut self, path: &Path) {
(**self).erase_spec(path)
}
fn set_field(&mut self, path: &Path, field: &str, value: Value) {
(**self).set_field(path, field, value)
}
fn erase_field(&mut self, path: &Path, field: &str) {
(**self).erase_field(path, field)
}
}
pub struct CowData<T: AbstractData> {
base: T,
overlay: HashMap<Path, Patch>,
}
pub enum Patch {
Created(SpecData),
Tombstone,
Edited {
set: Vec<(String, Value)>,
erased: HashSet<String>,
},
}
impl Patch {
fn edited(erased: HashSet<String>) -> Self {
Patch::Edited {
set: Vec::new(),
erased,
}
}
}
impl<T: AbstractData> CowData<T> {
pub fn new(base: T) -> Self {
Self {
base,
overlay: HashMap::new(),
}
}
pub fn is_empty(&self) -> bool {
self.overlay.is_empty()
}
pub fn commit(&mut self) {
for (path, patch) in self.overlay.drain() {
match patch {
Patch::Created(spec) => {
self.base.create_spec(path.clone(), spec.ty);
for (field, value) in spec.fields {
self.base.set_field(&path, &field, value);
}
}
Patch::Tombstone => self.base.erase_spec(&path),
Patch::Edited { set, erased } => {
for (field, value) in set {
self.base.set_field(&path, &field, value);
}
for field in erased {
self.base.erase_field(&path, &field);
}
}
}
}
}
pub fn rollback(&mut self) {
self.overlay.clear();
}
pub fn into_inner(self) -> T {
self.base
}
pub fn base(&self) -> &T {
&self.base
}
pub fn base_mut(&mut self) -> &mut T {
&mut self.base
}
pub fn overlay(&self) -> &HashMap<Path, Patch> {
&self.overlay
}
fn staged(&self, path: &Path) -> Option<&Patch> {
if self.overlay.is_empty() {
None
} else {
self.overlay.get(path)
}
}
}
impl<T: AbstractData> AbstractData for CowData<T> {
fn has_spec(&self, path: &Path) -> bool {
match self.staged(path) {
Some(Patch::Tombstone) => false,
Some(_) => true,
None => self.base.has_spec(path),
}
}
fn has_field(&self, path: &Path, field: &str) -> bool {
match self.staged(path) {
Some(Patch::Created(spec)) => spec.contains(field),
Some(Patch::Tombstone) => false,
Some(Patch::Edited { set, erased }) => {
!erased.contains(field) && (set.iter().any(|(f, _)| f == field) || self.base.has_field(path, field))
}
None => self.base.has_field(path, field),
}
}
fn spec_type(&self, path: &Path) -> Option<SpecType> {
match self.staged(path) {
Some(Patch::Created(spec)) => Some(spec.ty),
Some(Patch::Tombstone) => None,
Some(Patch::Edited { .. }) | None => self.base.spec_type(path),
}
}
fn try_field(&self, path: &Path, field: &str) -> Result<Option<Cow<'_, Value>>, DataError> {
match self.staged(path) {
Some(Patch::Created(spec)) => Ok(spec.get(field).map(Cow::Borrowed)),
Some(Patch::Tombstone) => Ok(None),
Some(Patch::Edited { set, erased }) => {
if erased.contains(field) {
Ok(None)
} else if let Some((_, value)) = set.iter().find(|(f, _)| f == field) {
Ok(Some(Cow::Borrowed(value)))
} else {
self.base.try_field(path, field)
}
}
None => self.base.try_field(path, field),
}
}
fn list_fields(&self, path: &Path) -> Option<Vec<String>> {
match self.staged(path) {
Some(Patch::Created(spec)) => Some(spec.fields.iter().map(|(k, _)| k.clone()).collect()),
Some(Patch::Tombstone) => None,
Some(Patch::Edited { set, erased }) => {
let mut names: Vec<String> = self
.base
.list_fields(path)
.unwrap_or_default()
.into_iter()
.filter(|f| !erased.contains(f))
.collect();
for (field, _) in set {
if !names.iter().any(|n| n == field) {
names.push(field.clone());
}
}
Some(names)
}
None => self.base.list_fields(path),
}
}
fn spec_paths(&self) -> Vec<Path> {
if self.overlay.is_empty() {
return self.base.spec_paths();
}
let mut paths: Vec<Path> = self
.base
.spec_paths()
.into_iter()
.filter(|p| !matches!(self.overlay.get(p), Some(Patch::Tombstone)))
.collect();
for (path, patch) in &self.overlay {
if matches!(patch, Patch::Created(_)) && !self.base.has_spec(path) {
paths.push(path.clone());
}
}
paths.sort_by(|a, b| a.as_str().cmp(b.as_str()));
paths
}
fn create_spec(&mut self, path: Path, ty: SpecType) {
self.overlay.insert(path, Patch::Created(SpecData::new(ty)));
}
fn erase_spec(&mut self, path: &Path) {
if self.base.has_spec(path) {
self.overlay.insert(path.clone(), Patch::Tombstone);
} else {
self.overlay.remove(path);
}
}
fn set_field(&mut self, path: &Path, field: &str, value: Value) {
if !self.overlay.contains_key(path) {
if self.base.has_spec(path) {
self.overlay.insert(path.clone(), Patch::edited(HashSet::new()));
} else {
debug_assert!(false, "set_field on absent spec at {path}");
return;
}
}
match self.overlay.get_mut(path).expect("entry ensured above") {
Patch::Created(spec) => spec.add(field, value),
Patch::Edited { set, erased } => {
erased.remove(field);
match set.iter_mut().find(|(f, _)| f == field) {
Some(slot) => slot.1 = value,
None => set.push((field.to_owned(), value)),
}
}
Patch::Tombstone => debug_assert!(false, "set_field on erased spec at {path}"),
}
}
fn erase_field(&mut self, path: &Path, field: &str) {
let base_has_field = self.base.has_field(path, field);
if !self.overlay.contains_key(path) {
if base_has_field {
let mut erased = HashSet::new();
erased.insert(field.to_owned());
self.overlay.insert(path.clone(), Patch::edited(erased));
}
return;
}
match self.overlay.get_mut(path).expect("entry present") {
Patch::Created(spec) => {
spec.remove(field);
}
Patch::Tombstone => {}
Patch::Edited { set, erased } => {
set.retain(|(f, _)| f != field);
if base_has_field {
erased.insert(field.to_owned());
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sdf::path;
#[test]
fn create_and_query_spec() {
let mut data = Data::new();
let root = path("/Root").unwrap();
data.create_spec(root.clone(), SpecType::Prim);
data.spec_mut(&root)
.unwrap()
.add("primChildren", Value::TokenVec(vec!["child".into()]));
assert!(data.has_spec(&root));
assert_eq!(data.spec_type(&root), Some(SpecType::Prim));
assert!(data.has_field(&root, "primChildren"));
assert_eq!(data.list_fields(&root).unwrap(), vec!["primChildren".to_string()]);
let value = data.get_field(&root, "primChildren").unwrap().into_owned();
assert_eq!(value, Value::TokenVec(vec!["child".into()]));
}
#[test]
fn erase_spec_removes() {
let mut data = Data::new();
let root = path("/Root").unwrap();
data.create_spec(root.clone(), SpecType::Prim);
assert!(data.has_spec(&root));
data.erase_spec(&root);
assert!(!data.has_spec(&root));
data.erase_spec(&root);
assert!(!data.has_spec(&root));
}
#[test]
fn paths_are_sorted() {
let mut data = Data::new();
data.create_spec(path("/Zebra").unwrap(), SpecType::Prim);
data.create_spec(path("/Apple").unwrap(), SpecType::Prim);
data.create_spec(path("/Mango").unwrap(), SpecType::Prim);
let paths = data.spec_paths();
let strs: Vec<&str> = paths.iter().map(|p| p.as_str()).collect();
assert_eq!(strs, vec!["/Apple", "/Mango", "/Zebra"]);
}
#[test]
fn from_abstract_copies_all_specs() {
let mut source = Data::new();
let root = path("/Root").unwrap();
source.create_spec(root.clone(), SpecType::Prim);
let root_spec = source.spec_mut(&root).unwrap();
root_spec.add("primChildren", Value::TokenVec(vec!["child".into()]));
root_spec.add("kind", Value::Token("component".into()));
let child = path("/Root/child").unwrap();
source.create_spec(child.clone(), SpecType::Prim);
source.spec_mut(&child).unwrap().add("specifier", Value::Int(0));
let copy = Data::from_abstract(&source as &dyn AbstractData).unwrap();
assert_eq!(copy.len(), 2);
assert_eq!(copy.list_fields(&root).unwrap(), vec!["primChildren", "kind"]);
assert_eq!(copy.get_field(&child, "specifier").unwrap().into_owned(), Value::Int(0));
}
#[test]
fn cow_reads_through() {
let mut base = Data::new();
let a = path("/A").unwrap();
base.create_spec(a.clone(), SpecType::Prim);
base.spec_mut(&a).unwrap().add("kind", Value::Token("group".into()));
let mut cow = CowData::new(base);
assert!(cow.is_empty());
assert!(cow.has_spec(&a));
assert_eq!(cow.get_field(&a, "kind").unwrap().into_owned(), Value::token("group"));
cow.set_field(&a, "kind", Value::token("component"));
assert!(!cow.is_empty());
assert_eq!(
cow.get_field(&a, "kind").unwrap().into_owned(),
Value::token("component")
);
let base = cow.into_inner();
assert_eq!(base.get_field(&a, "kind").unwrap().into_owned(), Value::token("group"));
}
#[test]
fn cow_commit_flushes() {
let mut base = Data::new();
let keep = path("/Keep").unwrap();
let doomed = path("/Doomed").unwrap();
base.create_spec(keep.clone(), SpecType::Prim);
base.create_spec(doomed.clone(), SpecType::Prim);
let mut cow = CowData::new(base);
let fresh = path("/Fresh").unwrap();
cow.create_spec(fresh.clone(), SpecType::Prim);
cow.set_field(&keep, "kind", Value::token("group"));
cow.erase_spec(&doomed);
cow.commit();
assert!(cow.is_empty());
let base = cow.into_inner();
assert!(base.has_spec(&fresh));
assert!(!base.has_spec(&doomed));
assert_eq!(
base.get_field(&keep, "kind").unwrap().into_owned(),
Value::token("group")
);
}
#[test]
fn cow_rollback_restores() {
let mut base = Data::new();
let a = path("/A").unwrap();
base.create_spec(a.clone(), SpecType::Prim);
let mut cow = CowData::new(base);
cow.create_spec(path("/B").unwrap(), SpecType::Prim);
cow.erase_spec(&a);
cow.rollback();
assert!(cow.is_empty());
assert!(cow.has_spec(&a));
assert!(!cow.has_spec(&path("/B").unwrap()));
}
#[test]
fn cow_spec_paths_merge() {
let mut base = Data::new();
base.create_spec(path("/B").unwrap(), SpecType::Prim);
base.create_spec(path("/D").unwrap(), SpecType::Prim);
let mut cow = CowData::new(base);
cow.create_spec(path("/A").unwrap(), SpecType::Prim);
cow.erase_spec(&path("/D").unwrap());
let paths = cow.spec_paths();
let paths: Vec<&str> = paths.iter().map(|p| p.as_str()).collect();
assert_eq!(paths, vec!["/A", "/B"]);
}
#[test]
fn cow_field_granular() {
let mut base = Data::new();
let a = path("/A").unwrap();
base.create_spec(a.clone(), SpecType::Prim);
let spec = base.spec_mut(&a).unwrap();
spec.add("kind", Value::Token("group".into()));
spec.add("doc", Value::String("hi".into()));
let mut cow = CowData::new(base);
cow.set_field(&a, "kind", Value::token("component"));
cow.set_field(&a, "custom", Value::Bool(true));
cow.erase_field(&a, "doc");
assert_eq!(
cow.get_field(&a, "kind").unwrap().into_owned(),
Value::token("component")
);
assert_eq!(cow.get_field(&a, "custom").unwrap().into_owned(), Value::Bool(true));
assert!(!cow.has_field(&a, "doc"));
assert_eq!(cow.list_fields(&a).unwrap(), vec!["kind", "custom"]);
cow.commit();
let base = cow.into_inner();
assert_eq!(
base.get_field(&a, "kind").unwrap().into_owned(),
Value::token("component")
);
assert_eq!(base.get_field(&a, "custom").unwrap().into_owned(), Value::Bool(true));
assert!(!base.has_field(&a, "doc"));
assert_eq!(base.list_fields(&a).unwrap(), vec!["kind", "custom"]);
}
}