aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/openapi.yaml3
-rw-r--r--src/api/store.rs15
-rw-r--r--src/document/store.rs53
-rw-r--r--src/document/typed.rs14
4 files changed, 50 insertions, 35 deletions
diff --git a/docs/openapi.yaml b/docs/openapi.yaml
index f188d69..4e07e8a 100644
--- a/docs/openapi.yaml
+++ b/docs/openapi.yaml
@@ -127,7 +127,8 @@ paths:
Typed JSON (`application/vnd.continuum.typed+json`) writes values of every
type, tagged (`{"$timestamp": …}`, `{"$bytes": …}`, `{"$uint": …}`,
- `{"$counter": …}`, `{"$float": …}`, `{"$text": …}`, `{"$map": …}`).
+ `{"$counter": …}`, `{"$text": …}`, `{"$map": …}`); a float is a number, and
+ a number without a fraction written over a float stays one.
A scalar of another type is also written as its media type (`api.md`, Types):
`application/octet-stream` (bytes), `application/vnd.continuum.int`, `.uint`,
`.float`, `.boolean`, `.counter` (decimal or `true`/`false`), or
diff --git a/src/api/store.rs b/src/api/store.rs
index 48bdafc..2b21c83 100644
--- a/src/api/store.rs
+++ b/src/api/store.rs
@@ -580,9 +580,10 @@ const TYPE_HEADER: &str = "x-continuum-type";
/// JSON, text as a string, or a scalar of a type of its own (`typed::from_native`).
struct WriteBody {
value: Val,
- /// Plain JSON or text, whose values keep the types of what they are written over
- /// where they can (`store::Writing::sticky`).
- sticky: bool,
+ /// Which of its values keep the types of what they are written over
+ /// (`store::Sticky`): plain JSON's and text's where they can, typed JSON's numbers
+ /// over floats.
+ sticky: store::Sticky,
/// How timestamps are written in it (`timestamp-format`).
timestamps: TimestampFormat,
}
@@ -600,19 +601,19 @@ fn body_value(headers: &HeaderMap, body: &Bytes) -> Result<WriteBody, ApiError>
};
let timestamps = json_form_of(params)?.timestamps;
let (value, sticky) = match media.as_str() {
- "application/json" => (Val::from_json(&json()?).map_err(bad)?, true),
+ "application/json" => (Val::from_json(&json()?).map_err(bad)?, store::Sticky::All),
typed::TYPED_JSON => (
Val::from_typed_json(&json()?, timestamps).map_err(bad)?,
- false,
+ store::Sticky::Floats,
),
typed::TEXT => (
String::from_utf8(body.to_vec())
.map(|text| Val::Scalar(automerge::ScalarValue::Str(text.into())))
.map_err(|_| bad("not UTF-8".to_owned()))?,
- true,
+ store::Sticky::All,
),
media => match typed::from_native(media, params, body) {
- Some(Ok(scalar)) => (Val::Scalar(scalar), false),
+ Some(Ok(scalar)) => (Val::Scalar(scalar), store::Sticky::No),
Some(Err(error)) => return Err(bad(error)),
None => return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into()),
},
diff --git a/src/document/store.rs b/src/document/store.rs
index 9075032..1a235b2 100644
--- a/src/document/store.rs
+++ b/src/document/store.rs
@@ -676,14 +676,27 @@ pub(crate) fn write_val_path(
Ok(writing.retyped)
}
+/// Which values a write writes as the type of what they are written over (`stuck`).
+#[derive(Clone, Copy, Debug, PartialEq, Eq)]
+pub(crate) enum Sticky {
+ /// None: the value says its type (a type's own media type, a cast).
+ No,
+ /// A number without a fraction over a float: typed JSON says every other type
+ /// itself, but not a float's, as JSON numbers don't keep `2.0` apart from `2` in
+ /// every language.
+ Floats,
+ /// Plain JSON, which has fewer types than Automerge: where it is the JSON form of a
+ /// value of the type there (a string that reads as a timestamp, as base64, a number
+ /// without a fraction over a float, an unsigned integer or a counter, a string over
+ /// a text).
+ All,
+}
+
/// How a write treats the types of what it writes over.
pub(crate) struct Writing {
- /// Types are sticky: the value comes as plain JSON, which has fewer types than
- /// Automerge, and where it is the JSON form of a value of the type there (a string
- /// that reads as a timestamp, as base64, a number without a fraction over a float,
- /// an unsigned integer or a counter, a string over a text), it is written as that
- /// type (`stuck`). Typed JSON and a type's own media type say the type themselves.
- pub(crate) sticky: bool,
+ /// Types are sticky: what the value is written as where it is the JSON form of a
+ /// value of the type there.
+ pub(crate) sticky: Sticky,
/// How sticky timestamps are written in JSON (`typed::TimestampFormat::json`).
pub(crate) timestamps: TimestampFormat,
/// Whether the write may change the type of what it writes over; if not, one that
@@ -695,10 +708,10 @@ pub(crate) struct Writing {
impl Writing {
pub(crate) fn sticky(timestamps: TimestampFormat) -> Self {
- Self::new(true, timestamps, true)
+ Self::new(Sticky::All, timestamps, true)
}
- pub(crate) fn new(sticky: bool, timestamps: TimestampFormat, retype: bool) -> Self {
+ pub(crate) fn new(sticky: Sticky, timestamps: TimestampFormat, retype: bool) -> Self {
Self {
sticky,
timestamps,
@@ -760,11 +773,23 @@ fn kind_of(held: &Value<'_>) -> Kind {
}
/// `value` written as the type of `held`, where it is a JSON form of a value of it
-/// (`Writing::sticky`); None where it isn't (it is written as it is).
-fn stuck(held: &Value<'_>, value: &Val, timestamps: TimestampFormat) -> Option<Val> {
+/// as `sticky` says (`Writing::sticky`); None where it isn't (it is written as it is).
+fn stuck(
+ held: &Value<'_>,
+ value: &Val,
+ sticky: Sticky,
+ timestamps: TimestampFormat,
+) -> Option<Val> {
let Val::Scalar(scalar) = value else {
return None;
};
+ let over_float = matches!(held, Value::Scalar(held) if matches!(held.as_ref(), ScalarValue::F64(_)))
+ && matches!(scalar, ScalarValue::Int(_) | ScalarValue::Uint(_));
+ match sticky {
+ Sticky::No => return None,
+ Sticky::Floats if !over_float => return None,
+ Sticky::Floats | Sticky::All => {}
+ }
let held = match held {
Value::Object(ObjType::Text) => {
return match scalar {
@@ -843,11 +868,7 @@ fn update_of<'v>(
let Some((held, _)) = holds.first() else {
return Ok((Update::Anew, Cow::Borrowed(value)));
};
- let value = match writing
- .sticky
- .then(|| stuck(held, value, writing.timestamps))
- .flatten()
- {
+ let value = match stuck(held, value, writing.sticky, writing.timestamps) {
Some(stuck) => Cow::Owned(stuck),
None => Cow::Borrowed(value),
};
@@ -1083,7 +1104,7 @@ pub(crate) fn cast_path(
.as_ref()
.ok_or_else(|| not_castable(format!("{} is cast to nothing", held.kind.name())))?;
let cast = typed::cast(held.kind, value, to, timestamps).map_err(not_castable)?;
- write_val_path(doc, path, &cast, Writing::new(false, timestamps, true))
+ write_val_path(doc, path, &cast, Writing::new(Sticky::No, timestamps, true))
}
/// Inserts `value` into a list: `path` ends in the index it goes to (the items from
diff --git a/src/document/typed.rs b/src/document/typed.rs
index 7f6e0cc..def06df 100644
--- a/src/document/typed.rs
+++ b/src/document/typed.rs
@@ -224,9 +224,9 @@ pub(crate) struct JsonForm {
// Typed JSON is plain JSON but for what plain JSON shows as another type: a single-key
// object whose key is a tag names it. `{"$uint": 5}`, `{"$counter": 5}`,
// `{"$timestamp": "2026-11-01T21:50:20.345Z"}` (in the timestamp format asked),
-// `{"$bytes": "<base64>"}`, `{"$float": 2}` (a float without a fraction, which JSON
-// numbers don't keep apart from integers in every language), `{"$text": "…"}` (an
-// Automerge text). A map whose only key starts with `$` is `{"$map": {…}}`, so that
+// `{"$bytes": "<base64>"}`, `{"$text": "…"}` (an Automerge text). A float is a number,
+// as in plain JSON; a number without a fraction written over a float stays one
+// (`store::Sticky::Floats`). A map whose only key starts with `$` is `{"$map": {…}}`, so that
// nothing else reads as a tag.
/// A map's single key that is a tag, or would be taken for one.
@@ -270,9 +270,6 @@ pub(crate) fn scalar_to_json(scalar: &ScalarValue, form: JsonForm) -> Option<Jso
return Some(tagged("$timestamp", form.timestamps.json(*t)));
}
ScalarValue::Bytes(b) => return Some(tagged("$bytes", BASE64.encode(b).into())),
- ScalarValue::F64(f) if f.fract() == 0.0 => {
- return Some(tagged("$float", serde_json::Number::from_f64(*f)?.into()));
- }
_ => {}
}
}
@@ -590,11 +587,6 @@ impl Val {
})?))
}
"$counter" => Val::Scalar(ScalarValue::counter(integer(value)?)),
- "$float" => Val::Scalar(ScalarValue::F64(
- value
- .as_f64()
- .ok_or_else(|| format!("$float: not a number: {value}"))?,
- )),
"$timestamp" => {
let text = match value {
JsonValue::String(text) => text.clone(),