diff options
Diffstat (limited to 'src/document/store.rs')
| -rw-r--r-- | src/document/store.rs | 200 |
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, ¤t, log)?)) + Ok(Some(resolved_to_json(doc, ¤t, 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 }, |
