aboutsummaryrefslogtreecommitdiffstats
path: root/src/document/store.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-02 10:26:51 +0100
committertv <tv@krebsco.de>2026-11-02 10:26:51 +0100
commitd54ac96f6acabb7fd39e0b642bf1cd919a6e82dd (patch)
tree8f36f28cd7afb2dc2847d08b30ced9892ac3688d /src/document/store.rs
parent9a73a89e5888df59bbdaff1279bd65e51264cd61 (diff)
api: add application/vnd.continuum.typed+json
Diffstat (limited to 'src/document/store.rs')
-rw-r--r--src/document/store.rs212
1 files changed, 82 insertions, 130 deletions
diff --git a/src/document/store.rs b/src/document/store.rs
index 0f1fd58..4dd9d02 100644
--- a/src/document/store.rs
+++ b/src/document/store.rs
@@ -9,7 +9,7 @@ use serde::Serialize;
use serde_json::Value as JsonValue;
use std::collections::BTreeSet;
-use super::typed::{self, JsonForm, Kind};
+use super::typed::{self, JsonForm, Kind, Val};
/// Store paths touched by one batch of document changes (`AppState::change_events`).
#[derive(Debug)]
@@ -333,7 +333,7 @@ fn resolved_to_json(
}
}
- Ok(JsonValue::Object(out))
+ Ok(typed::map_to_json(out, form))
}
fn object_to_json(
@@ -357,7 +357,7 @@ fn object_to_json(
}
Ok(JsonValue::Array(out))
}
- ObjType::Text => Ok(JsonValue::String(doc.text(id)?)),
+ ObjType::Text => Ok(typed::text_to_json(doc.text(id)?, form)),
ObjType::Table => Err(anyhow!("Automerge tables are not supported").into()),
}
}
@@ -632,6 +632,16 @@ pub(crate) fn write_path(
path: &[String],
value: &JsonValue,
) -> Result<(), StoreError> {
+ write_val_path(doc, path, &from_json(value)?)
+}
+
+/// [`write_path`] for a value of any type (`typed::Val`): from typed JSON, or a scalar
+/// of a type JSON doesn't have.
+pub(crate) fn write_val_path(
+ doc: &mut impl Transactable,
+ path: &[String],
+ value: &Val,
+) -> Result<(), StoreError> {
let (leaf, parents) = path.split_last().ok_or(StoreError::NotFound)?;
match parent_for_write(doc, parents)? {
Parent::Maps(maps) => update_key(doc, &maps, leaf, value)?,
@@ -651,67 +661,18 @@ 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(())
+fn from_json(value: &JsonValue) -> Result<Val, StoreError> {
+ Val::from_json(value).map_err(|error| anyhow!(error).into())
}
// 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
-// does the same item by item, by position; a scalar equal to the one there isn't
-// written again. So a write of a whole map, concurrent with one inside it elsewhere,
-// keeps the other unless it changes the same key. Where the path holds concurrent
-// values (a conflict), or one of another kind, it is written anew: the write ends the
-// conflict.
-
-/// What `value` would be stored as, if it is a scalar.
-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),
- JsonValue::Number(value) => {
- if let Some(value) = value.as_i64() {
- ScalarValue::Int(value)
- } else if let Some(value) = value.as_u64() {
- ScalarValue::Uint(value)
- } else if let Some(value) = value.as_f64() {
- ScalarValue::F64(value)
- } else {
- return Err(anyhow!("Unsupported JSON number").into());
- }
- }
- JsonValue::Null => ScalarValue::Null,
- JsonValue::Object(_) | JsonValue::Array(_) => return Ok(None),
- }))
-}
+// does the same item by item, by position; a text written over a text is changed
+// where it differs (a diff, as splices); a scalar equal to the one there isn't written
+// again. So a write of a whole map, concurrent with one inside it elsewhere, keeps the
+// other unless it changes the same key. Where the path holds concurrent values (a
+// conflict), or one of another kind, it is written anew: the write ends the conflict.
/// What updating a place to a value comes to, given what it holds (all its concurrent
/// values).
@@ -722,22 +683,23 @@ enum Update {
Map(ObjId),
/// A list there, the only value: its items updated.
List(ObjId),
+ /// A text there, the only value: changed where it differs.
+ Text(ObjId),
/// Written anew.
Anew,
}
-fn update_of(holds: &[(Value<'static>, ObjId)], value: &JsonValue) -> Result<Update, StoreError> {
+fn update_of(holds: &[(Value<'static>, ObjId)], value: &Val) -> Update {
let [(held, id)] = holds else {
- return Ok(Update::Anew);
+ return Update::Anew;
};
- Ok(match (held, value) {
- (Value::Object(ObjType::Map), JsonValue::Object(_)) => Update::Map(id.clone()),
- (Value::Object(ObjType::List), JsonValue::Array(_)) => Update::List(id.clone()),
- (Value::Scalar(held), _) if json_scalar(value)?.as_ref() == Some(held.as_ref()) => {
- Update::Same
- }
+ match (held, value) {
+ (Value::Object(ObjType::Map), Val::Map(_)) => Update::Map(id.clone()),
+ (Value::Object(ObjType::List), Val::List(_)) => Update::List(id.clone()),
+ (Value::Object(ObjType::Text), Val::Text(_)) => Update::Text(id.clone()),
+ (Value::Scalar(held), Val::Scalar(scalar)) if held.as_ref() == scalar => Update::Same,
_ => Update::Anew,
- })
+ }
}
/// Updates `key` of the merged maps `maps` to `value`.
@@ -745,16 +707,17 @@ fn update_key(
doc: &mut impl Transactable,
maps: &[ObjId],
key: &str,
- value: &JsonValue,
+ value: &Val,
) -> Result<(), StoreError> {
- match update_of(&candidates(doc, maps, key)?, value)? {
+ match update_of(&candidates(doc, maps, key)?, value) {
Update::Same => Ok(()),
Update::Map(map) => update_map(doc, &map, value),
Update::List(list) => update_list(doc, &list, value),
+ Update::Text(text) => update_text(doc, &text, value),
Update::Anew => {
// Cleared in every merged map, or an old map would merge into the new value.
delete_in_all(doc, maps, key)?;
- json_to_automerge(doc, &maps[0], key, value)
+ write_value(doc, &maps[0], Slot::Key(key), value)
}
}
}
@@ -764,44 +727,37 @@ fn update_item(
doc: &mut impl Transactable,
list: &ObjId,
index: usize,
- value: &JsonValue,
+ value: &Val,
) -> Result<(), StoreError> {
- match update_of(&candidates(doc, std::slice::from_ref(list), index)?, value)? {
+ match update_of(&candidates(doc, std::slice::from_ref(list), index)?, value) {
Update::Same => Ok(()),
Update::Map(map) => update_map(doc, &map, value),
Update::List(inner) => update_list(doc, &inner, value),
+ Update::Text(text) => update_text(doc, &text, value),
Update::Anew => write_value(doc, list, Slot::At(index), value),
}
}
-fn update_map(
- doc: &mut impl Transactable,
- map: &ObjId,
- value: &JsonValue,
-) -> Result<(), StoreError> {
- let JsonValue::Object(object) = value else {
- unreachable!("a map is updated to an object");
+fn update_map(doc: &mut impl Transactable, map: &ObjId, value: &Val) -> Result<(), StoreError> {
+ let Val::Map(entries) = value else {
+ unreachable!("a map is updated to a map");
};
let maps = std::slice::from_ref(map);
// The keys it loses first, then those it keeps or gets, each in key order.
for key in doc.keys(map).collect::<Vec<_>>() {
- if !object.contains_key(&key) {
+ if !entries.iter().any(|(kept, _)| *kept == key) {
doc.delete(map, key.as_str())?;
}
}
- for (key, value) in object {
+ for (key, value) in entries {
update_key(doc, maps, key, value)?;
}
Ok(())
}
-fn update_list(
- doc: &mut impl Transactable,
- list: &ObjId,
- value: &JsonValue,
-) -> Result<(), StoreError> {
- let JsonValue::Array(items) = value else {
- unreachable!("a list is updated to an array");
+fn update_list(doc: &mut impl Transactable, list: &ObjId, value: &Val) -> Result<(), StoreError> {
+ let Val::List(items) = value else {
+ unreachable!("a list is updated to a list");
};
let length = doc.length(list);
for (index, item) in items.iter().enumerate() {
@@ -817,6 +773,14 @@ fn update_list(
Ok(())
}
+fn update_text(doc: &mut impl Transactable, text: &ObjId, value: &Val) -> Result<(), StoreError> {
+ let Val::Text(new) = value else {
+ unreachable!("a text is updated to a text");
+ };
+ doc.update_text(text, new)?;
+ Ok(())
+}
+
/// What a write's last segment goes into.
enum Parent {
/// Merged maps (`[0]` takes new keys).
@@ -912,23 +876,23 @@ fn counter_at(doc: &impl ReadDoc, parent: &ObjId, prop: Prop) -> Result<i64, Sto
/// there on move up one; the list's length appends). An index past the end is
/// `PastTheEnd`; a parent that isn't a list `NotAList`; anything but an index there, or
/// no parent, `NotFound`.
+#[cfg(test)]
pub(crate) fn insert_path(
doc: &mut impl Transactable,
path: &[String],
value: &JsonValue,
) -> Result<(), StoreError> {
- let (list, index) = insertion(doc, path)?;
- write_value(doc, &list, Slot::Index(index), value)
+ insert_val_path(doc, path, &from_json(value)?)
}
-/// [`insert_path`] for a scalar (`typed::from_native`).
-pub(crate) fn insert_scalar_path(
+/// [`insert_path`] for a value of any type (`typed::Val`).
+pub(crate) fn insert_val_path(
doc: &mut impl Transactable,
path: &[String],
- scalar: &ScalarValue,
+ value: &Val,
) -> Result<(), StoreError> {
let (list, index) = insertion(doc, path)?;
- put_scalar(doc, &list, Slot::Index(index), scalar.clone())
+ write_value(doc, &list, Slot::Index(index), value)
}
/// The list an insert at `path` goes into, and where (`insert_path`).
@@ -1216,55 +1180,43 @@ fn write_value(
doc: &mut impl Transactable,
parent: &ObjId,
slot: Slot,
- value: &JsonValue,
+ value: &Val,
) -> Result<(), StoreError> {
match value {
- JsonValue::Object(_) | JsonValue::Array(_) => {
+ Val::Scalar(scalar) => put_scalar(doc, parent, slot, scalar.clone()),
+ tree => {
// A new tree in one go, not an operation at a time.
- let tree = hydrated(value)?;
+ let tree = hydrated(tree, doc.text_encoding());
let (prop, insert): (Prop, bool) = match slot {
Slot::Key(key) => (key.into(), false),
Slot::Index(index) => (index.into(), true),
Slot::At(index) => (index.into(), false),
};
doc.batch_create_object(parent, prop, &tree, insert)?;
- }
- scalar => {
- let scalar = json_scalar(scalar)?.expect("neither an object nor an array");
- put_scalar(doc, parent, slot, scalar)?;
+ Ok(())
}
}
-
- Ok(())
}
-/// `value` as Automerge holds it: maps, lists and scalars as `write_value` stores them.
-fn hydrated(value: &JsonValue) -> Result<automerge::hydrate::Value, StoreError> {
+/// `value` as Automerge holds it.
+fn hydrated(value: &Val, encoding: automerge::TextEncoding) -> automerge::hydrate::Value {
use automerge::hydrate;
- Ok(match value {
- JsonValue::Object(object) => {
- let mut map = std::collections::HashMap::new();
- for (key, value) in object {
- map.insert(key.clone(), hydrated(value)?);
- }
- hydrate::Value::Map(hydrate::Map::from(map))
- }
- JsonValue::Array(items) => {
- hydrate::Value::from(items.iter().map(hydrated).collect::<Result<Vec<_>, _>>()?)
- }
- scalar => {
- hydrate::Value::from(json_scalar(scalar)?.expect("neither an object nor an array"))
- }
- })
-}
-
-fn json_to_automerge(
- doc: &mut impl Transactable,
- parent: &ObjId,
- key: &str,
- value: &JsonValue,
-) -> Result<(), StoreError> {
- write_value(doc, parent, Slot::Key(key), value)
+ match value {
+ Val::Map(entries) => hydrate::Value::Map(hydrate::Map::from(
+ entries
+ .iter()
+ .map(|(key, value)| (key.clone(), hydrated(value, encoding)))
+ .collect::<std::collections::HashMap<_, _>>(),
+ )),
+ Val::List(items) => hydrate::Value::from(
+ items
+ .iter()
+ .map(|item| hydrated(item, encoding))
+ .collect::<Vec<_>>(),
+ ),
+ Val::Text(text) => hydrate::Value::text(encoding, text),
+ Val::Scalar(scalar) => hydrate::Value::Scalar(scalar.clone()),
+ }
}
fn scalar_to_json(scalar: ScalarValue, form: JsonForm) -> Result<JsonValue, StoreError> {