From 58d574166fef1246f367009546358703e16d7f7e Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 2 Nov 2026 10:26:51 +0100 Subject: api: add application/vnd.continuum.typed+json --- src/document/store.rs | 212 +++++++++++++++++++------------------------------- 1 file changed, 82 insertions(+), 130 deletions(-) (limited to 'src/document/store.rs') 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()), } } @@ -631,6 +631,16 @@ pub(crate) fn write_path( doc: &mut impl Transactable, 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)? { @@ -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::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, 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 { +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::>() { - 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 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 { +/// `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::, _>>()?) - } - 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::>(), + )), + Val::List(items) => hydrate::Value::from( + items + .iter() + .map(|item| hydrated(item, encoding)) + .collect::>(), + ), + 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 { -- cgit v1.3.1