aboutsummaryrefslogtreecommitdiffstats
path: root/src/document/store.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/document/store.rs')
-rw-r--r--src/document/store.rs200
1 files changed, 155 insertions, 45 deletions
diff --git a/src/document/store.rs b/src/document/store.rs
index cc67f77..0f1fd58 100644
--- a/src/document/store.rs
+++ b/src/document/store.rs
@@ -5,11 +5,12 @@ use automerge::{
ActorId, Automerge, ObjId, ObjType, Prop, ReadDoc, ScalarValue, Value,
transaction::Transactable,
};
-use base64::{Engine, engine::general_purpose::STANDARD as BASE64};
use serde::Serialize;
use serde_json::Value as JsonValue;
use std::collections::BTreeSet;
+use super::typed::{self, JsonForm, Kind};
+
/// Store paths touched by one batch of document changes (`AppState::change_events`).
#[derive(Debug)]
pub(crate) struct ChangeSet {
@@ -283,14 +284,15 @@ fn merged_keys(doc: &impl ReadDoc, maps: &[ObjId]) -> BTreeSet<String> {
fn resolved_to_json(
doc: &impl ReadDoc,
resolved: &Resolved,
+ form: JsonForm,
mut log: Option<&mut ConflictLog>,
) -> Result<JsonValue, StoreError> {
let maps = match resolved {
Resolved::Maps(maps) => maps,
Resolved::Other(Value::Scalar(scalar), _) => {
- return scalar_to_json(scalar.clone().into_owned());
+ return scalar_to_json(scalar.clone().into_owned(), form);
}
- Resolved::Other(Value::Object(_), id) => return object_to_json(doc, id, log),
+ Resolved::Other(Value::Object(_), id) => return object_to_json(doc, id, form, log),
};
let mut out = serde_json::Map::new();
@@ -313,7 +315,9 @@ fn resolved_to_json(
}
};
- let json = nested(&mut log, &key, |log| resolved_to_json(doc, &child, log))?;
+ let json = nested(&mut log, &key, |log| {
+ resolved_to_json(doc, &child, form, log)
+ })?;
out.insert(key, json);
}
} else {
@@ -322,7 +326,9 @@ fn resolved_to_json(
let Some(child) = resolve(doc, maps, &key, log.as_deref_mut())? else {
continue;
};
- let json = nested(&mut log, &key, |log| resolved_to_json(doc, &child, log))?;
+ let json = nested(&mut log, &key, |log| {
+ resolved_to_json(doc, &child, form, log)
+ })?;
out.insert(key, json);
}
}
@@ -333,19 +339,20 @@ fn resolved_to_json(
fn object_to_json(
doc: &impl ReadDoc,
id: &ObjId,
+ form: JsonForm,
mut log: Option<&mut ConflictLog>,
) -> Result<JsonValue, StoreError> {
match doc.object_type(id)? {
- ObjType::Map => resolved_to_json(doc, &Resolved::Maps(vec![id.clone()]), log),
+ ObjType::Map => resolved_to_json(doc, &Resolved::Maps(vec![id.clone()]), form, log),
ObjType::List => {
let mut out = Vec::new();
for (index, item) in doc.list_range(id, ..).enumerate() {
let child_id = item.id();
out.push(match Value::from(item.value) {
Value::Object(_) => nested(&mut log, &index.to_string(), |log| {
- object_to_json(doc, &child_id, log)
+ object_to_json(doc, &child_id, form, log)
})?,
- Value::Scalar(scalar) => scalar_to_json(scalar.into_owned())?,
+ Value::Scalar(scalar) => scalar_to_json(scalar.into_owned(), form)?,
});
}
Ok(JsonValue::Array(out))
@@ -367,12 +374,13 @@ fn delete_in_all(doc: &mut impl Transactable, maps: &[ObjId], key: &str) -> Resu
fn read_path_inner(
doc: &impl ReadDoc,
path: &[String],
+ form: JsonForm,
mut log: Option<&mut ConflictLog>,
) -> Result<Option<JsonValue>, StoreError> {
let Some(current) = resolve_logged(doc, path, log.as_deref_mut())? else {
return Ok(None);
};
- Ok(Some(resolved_to_json(doc, &current, log)?))
+ Ok(Some(resolved_to_json(doc, &current, form, log)?))
}
/// Reads a path; the empty path is the whole store.
@@ -380,21 +388,82 @@ pub(crate) fn read_path(
doc: &impl ReadDoc,
path: &[String],
) -> Result<Option<JsonValue>, StoreError> {
- read_path_inner(doc, path, None)
+ read_path_as(doc, path, JsonForm::default())
+}
+
+/// [`read_path`], in JSON of `form`.
+pub(crate) fn read_path_as(
+ doc: &impl ReadDoc,
+ path: &[String],
+ form: JsonForm,
+) -> Result<Option<JsonValue>, StoreError> {
+ read_path_inner(doc, path, form, None)
}
+#[cfg(test)]
pub(crate) fn read_path_with_conflicts(
doc: &impl ReadDoc,
path: &[String],
) -> Result<Option<(JsonValue, Vec<ConflictReport>)>, StoreError> {
+ read_path_with_conflicts_as(doc, path, JsonForm::default())
+}
+
+/// [`read_path_with_conflicts`], in JSON of `form`.
+pub(crate) fn read_path_with_conflicts_as(
+ doc: &impl ReadDoc,
+ path: &[String],
+ form: JsonForm,
+) -> Result<Option<(JsonValue, Vec<ConflictReport>)>, StoreError> {
let mut log = ConflictLog {
want_values: true,
+ form,
..Default::default()
};
- let value = read_path_inner(doc, path, Some(&mut log))?;
+ let value = read_path_inner(doc, path, form, Some(&mut log))?;
Ok(value.map(|value| (value, log.reports)))
}
+/// What is stored at `path` (the empty path: the root map), and, where it is a scalar
+/// or a text, the value itself, to show natively (`typed::native`); None if nothing is
+/// there.
+pub(crate) fn read_held(doc: &impl ReadDoc, path: &[String]) -> Result<Option<Held>, StoreError> {
+ let Some(resolved) = resolve_through(doc, path)? else {
+ return Ok(None);
+ };
+ Ok(Some(match resolved {
+ Resolved::Maps(_) | Resolved::Other(Value::Object(ObjType::Map), _) => Held {
+ kind: Kind::Map,
+ value: None,
+ },
+ Resolved::Other(Value::Object(ObjType::List), _) => Held {
+ kind: Kind::List,
+ value: None,
+ },
+ Resolved::Other(Value::Object(ObjType::Text), id) => Held {
+ kind: Kind::Text,
+ value: Some(ScalarValue::Str(doc.text(&id)?.into())),
+ },
+ Resolved::Other(Value::Object(ObjType::Table), _) => {
+ return Err(anyhow!("Automerge tables are not supported").into());
+ }
+ Resolved::Other(Value::Scalar(scalar), _) => {
+ let scalar = scalar.into_owned();
+ Held {
+ kind: Kind::of(&scalar),
+ value: Some(scalar),
+ }
+ }
+ }))
+}
+
+/// What a path holds (`read_held`).
+#[derive(Debug, Clone)]
+pub(crate) struct Held {
+ pub(crate) kind: Kind,
+ /// A scalar, or a text's characters as a string.
+ pub(crate) value: Option<ScalarValue>,
+}
+
/// For streams: the value, plus whether the path or subtree involves any conflict.
/// Reports are collected without values, so this costs little more than read_path.
pub(crate) fn read_path_traced(
@@ -402,7 +471,7 @@ pub(crate) fn read_path_traced(
path: &[String],
) -> Result<(Option<JsonValue>, bool), StoreError> {
let mut log = ConflictLog::default();
- let value = read_path_inner(doc, path, Some(&mut log))?;
+ let value = read_path_inner(doc, path, JsonForm::default(), Some(&mut log))?;
Ok((value, !log.reports.is_empty()))
}
@@ -506,20 +575,21 @@ fn summary(
return Err(anyhow!("Automerge tables are not supported").into());
}
Resolved::Other(Value::Scalar(scalar), _) => {
- let (kind, size) = match &**scalar {
- ScalarValue::Str(text) => ("string", Some(text.len())),
- ScalarValue::Bytes(bytes) => ("bytes", Some(bytes.len())),
- ScalarValue::Null => ("null", None),
- ScalarValue::Boolean(_) => ("boolean", None),
- ScalarValue::Unknown { .. } => ("unknown", None),
- _ => ("number", None),
+ let kind = Kind::of(scalar);
+ let size = match &**scalar {
+ ScalarValue::Str(text) => Some(text.len()),
+ ScalarValue::Bytes(bytes) => Some(bytes.len()),
+ _ => None,
};
- out.insert("type".into(), kind.into());
+ out.insert("type".into(), kind.name().into());
if let Some(size) = size {
out.insert("size".into(), size.into());
}
- if kind != "unknown" && size.is_none_or(|size| size <= SHALLOW_INLINE) {
- out.insert("value".into(), scalar_to_json(scalar.clone().into_owned())?);
+ if kind != Kind::Unknown && size.is_none_or(|size| size <= SHALLOW_INLINE) {
+ out.insert(
+ "value".into(),
+ scalar_to_json(scalar.clone().into_owned(), JsonForm::default())?,
+ );
}
}
}
@@ -547,8 +617,8 @@ pub(crate) fn read_candidates(
candidates(doc, &maps, leaf.as_str())?
.into_iter()
.map(|(value, id)| match value {
- Value::Scalar(scalar) => scalar_to_json(scalar.into_owned()),
- Value::Object(_) => object_to_json(doc, &id, None),
+ Value::Scalar(scalar) => scalar_to_json(scalar.into_owned(), JsonForm::default()),
+ Value::Object(_) => object_to_json(doc, &id, JsonForm::default(), None),
})
.collect()
}
@@ -581,6 +651,38 @@ pub(crate) fn write_path(
Ok(())
}
+/// [`write_path`] for a scalar (`typed::from_native`): one of a type JSON doesn't have,
+/// or doesn't tell apart (bytes, a timestamp, an unsigned integer, a counter).
+pub(crate) fn write_scalar_path(
+ doc: &mut impl Transactable,
+ path: &[String],
+ scalar: &ScalarValue,
+) -> Result<(), StoreError> {
+ let (leaf, parents) = path.split_last().ok_or(StoreError::NotFound)?;
+ let same = |holds: &[(Value<'static>, ObjId)]| matches!(holds, [(Value::Scalar(held), _)] if held.as_ref() == scalar);
+ match parent_for_write(doc, parents)? {
+ Parent::Maps(maps) => {
+ if !same(&candidates(doc, &maps, leaf.as_str())?) {
+ delete_in_all(doc, &maps, leaf)?;
+ put_scalar(doc, &maps[0], Slot::Key(leaf), scalar.clone())?;
+ }
+ }
+ Parent::List(list) => {
+ let index = list_index(leaf).ok_or(StoreError::NotFound)?;
+ let length = doc.length(&list);
+ if index > length {
+ return Err(StoreError::NotFound);
+ }
+ if index == length {
+ put_scalar(doc, &list, Slot::Index(index), scalar.clone())?;
+ } else if !same(&candidates(doc, std::slice::from_ref(&list), index)?) {
+ put_scalar(doc, &list, Slot::At(index), scalar.clone())?;
+ }
+ }
+ }
+ Ok(())
+}
+
// A write makes its path read as the value written, changing only what differs: a map
// written over a map keeps its keys that stay (and what is inside them), and gets
// those it lacks and loses those the value doesn't have; a list written over a list
@@ -591,7 +693,7 @@ pub(crate) fn write_path(
// conflict.
/// What `value` would be stored as, if it is a scalar.
-fn json_scalar(value: &JsonValue) -> Result<Option<ScalarValue>, StoreError> {
+pub(crate) fn json_scalar(value: &JsonValue) -> Result<Option<ScalarValue>, StoreError> {
Ok(Some(match value {
JsonValue::String(value) => ScalarValue::Str(value.as_str().into()),
JsonValue::Bool(value) => ScalarValue::Boolean(*value),
@@ -815,6 +917,22 @@ pub(crate) fn insert_path(
path: &[String],
value: &JsonValue,
) -> Result<(), StoreError> {
+ let (list, index) = insertion(doc, path)?;
+ write_value(doc, &list, Slot::Index(index), value)
+}
+
+/// [`insert_path`] for a scalar (`typed::from_native`).
+pub(crate) fn insert_scalar_path(
+ doc: &mut impl Transactable,
+ path: &[String],
+ scalar: &ScalarValue,
+) -> Result<(), StoreError> {
+ let (list, index) = insertion(doc, path)?;
+ put_scalar(doc, &list, Slot::Index(index), scalar.clone())
+}
+
+/// The list an insert at `path` goes into, and where (`insert_path`).
+fn insertion(doc: &impl ReadDoc, path: &[String]) -> Result<(ObjId, usize), StoreError> {
let (leaf, parents) = path.split_last().ok_or(StoreError::NotFound)?;
let list = match resolve_through(doc, parents)? {
None => return Err(StoreError::NotFound),
@@ -826,8 +944,7 @@ pub(crate) fn insert_path(
if index > length {
return Err(StoreError::PastTheEnd { index, length });
}
- write_value(doc, &list, Slot::Index(index), value)?;
- Ok(())
+ Ok((list, index))
}
/// Makes `dst` hold at `path` what `src`'s merged view holds there: a type-preserving
@@ -1150,21 +1267,11 @@ fn json_to_automerge(
write_value(doc, parent, Slot::Key(key), value)
}
-fn scalar_to_json(scalar: ScalarValue) -> Result<JsonValue, StoreError> {
- match scalar {
- ScalarValue::Null => Ok(JsonValue::Null),
- ScalarValue::Boolean(b) => Ok(JsonValue::Bool(b)),
- ScalarValue::Int(i) => Ok(JsonValue::Number(i.into())),
- ScalarValue::Uint(u) => Ok(JsonValue::Number(u.into())),
- ScalarValue::F64(f) => serde_json::Number::from_f64(f)
- .map(JsonValue::Number)
- .ok_or(anyhow!("Non-finite float cannot be represented as JSON").into()),
- ScalarValue::Str(s) => Ok(JsonValue::String(s.to_string())),
- ScalarValue::Bytes(b) => Ok(JsonValue::String(BASE64.encode(b))),
- ScalarValue::Counter(c) => Ok(JsonValue::Number(i64::from(c).into())),
- ScalarValue::Timestamp(t) => Ok(JsonValue::Number(t.into())),
- ScalarValue::Unknown { .. } => Err(anyhow!("Unsupported Automerge scalar type").into()),
- }
+fn scalar_to_json(scalar: ScalarValue, form: JsonForm) -> Result<JsonValue, StoreError> {
+ typed::scalar_to_json(&scalar, form).ok_or_else(|| match scalar {
+ ScalarValue::F64(_) => anyhow!("Non-finite float cannot be represented as JSON").into(),
+ _ => anyhow!("Unsupported Automerge scalar type").into(),
+ })
}
// --- conflict log ------------------------------------------------------------
@@ -1204,6 +1311,8 @@ struct ConflictLog {
want_values: bool,
/// Currently walking the ancestors of the requested path.
in_ancestors: bool,
+ /// How candidate values are shown.
+ form: JsonForm,
}
fn op_id_string(id: &ObjId) -> String {
@@ -1217,10 +1326,11 @@ fn value_to_json(
doc: &impl ReadDoc,
value: &Value<'_>,
id: &ObjId,
+ form: JsonForm,
) -> Result<JsonValue, StoreError> {
match value {
- Value::Scalar(scalar) => Ok(scalar_to_json(scalar.clone().into_owned())?),
- Value::Object(_) => object_to_json(doc, id, None),
+ Value::Scalar(scalar) => Ok(scalar_to_json(scalar.clone().into_owned(), form)?),
+ Value::Object(_) => object_to_json(doc, id, form, None),
}
}
@@ -1247,7 +1357,7 @@ impl ConflictLog {
op_id: op_id_string(id),
selected,
value: if with_values {
- Some(value_to_json(doc, value, id)?)
+ Some(value_to_json(doc, value, id, self.form)?)
} else {
None
},