diff options
| author | tv <tv@krebsco.de> | 2026-11-02 11:07:46 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-02 11:07:46 +0100 |
| commit | 8033c4dd17a637c21e1dd0c536edc04190851a21 (patch) | |
| tree | 34ac46ca8233fe32bd33b581f150e075d4fcd42d /src | |
| parent | 2ad5d7c6ba0962e7ba2a8c919cddc1c2fa136f63 (diff) | |
api: drop $float as JSON number is OK
Diffstat (limited to 'src')
| -rw-r--r-- | src/api/store.rs | 15 | ||||
| -rw-r--r-- | src/document/store.rs | 53 | ||||
| -rw-r--r-- | src/document/typed.rs | 14 |
3 files changed, 48 insertions, 34 deletions
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(), |
