From d54ac96f6acabb7fd39e0b642bf1cd919a6e82dd 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/api/store.rs | 126 ++++++++++++++++++++++++++++++------------------------- 1 file changed, 68 insertions(+), 58 deletions(-) (limited to 'src/api') diff --git a/src/api/store.rs b/src/api/store.rs index 13f5012..47aade1 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -23,7 +23,7 @@ use super::{ApiError, store_status}; use crate::document::ChangeHash; use crate::document::meta; use crate::document::store::{self, StoreError, paths_related}; -use crate::document::typed::{self, JsonForm, Kind, TimestampFormat}; +use crate::document::typed::{self, JsonForm, Kind, TimestampFormat, Val}; use crate::state::{AppState, ReadSource}; use crate::transport::Hex; @@ -310,13 +310,12 @@ pub(crate) async fn put_store_entry( } let value = body_value(&headers, &body)?; // Answered with the value written, shown as asked. - let (held, json) = value.held(); - let shown = shown(headers.get(ACCEPT), held.kind)?; + let shown = shown(headers.get(ACCEPT), value.kind())?; + let (held, json) = written(&value, shown); let ((), heads) = state - .mutate_doc_for(Some(user.0.clone()), move |doc| match &value { - Written::Json(value) => store::write_path(doc, &path, value), - Written::Scalar(scalar) => store::write_scalar_path(doc, &path, scalar), + .mutate_doc_for(Some(user.0.clone()), move |doc| { + store::write_val_path(doc, &path, &value) }) .await .map_err(|e| store_status(e, "Failed to write store value"))?; @@ -378,8 +377,15 @@ enum Shown { /// a timestamp format there is none of. fn json_form(accept: Option<&HeaderValue>) -> Result { for (media, params) in accepted(accept) { - if media == "application/json" { - return json_form_of(params); + match media.as_str() { + "application/json" => return json_form_of(params), + typed::TYPED_JSON => { + return Ok(JsonForm { + typed: true, + ..json_form_of(params)? + }); + } + _ => {} } } Ok(JsonForm::default()) @@ -395,7 +401,10 @@ fn json_form_of(params: &str) -> Result { ) })?, }; - Ok(JsonForm { timestamps }) + Ok(JsonForm { + timestamps, + typed: false, + }) } /// The media types of `accept`, in its order, each with its parameters (q-values are not @@ -425,6 +434,12 @@ fn shown(accept: Option<&HeaderValue>, kind: Kind) -> Result { for (media, params) in &items { match media.as_str() { "application/json" => return Ok(Shown::Json(json_form_of(params)?)), + typed::TYPED_JSON => { + return Ok(Shown::Json(JsonForm { + typed: true, + ..json_form_of(params)? + })); + } typed::TEXT => return Ok(Shown::Text), "*/*" | "application/*" => return Ok(Shown::Json(JsonForm::default())), media if Some(media) == native => { @@ -442,7 +457,7 @@ fn shown(accept: Option<&HeaderValue>, kind: Kind) -> Result { _ => {} } } - let mut offered = vec!["application/json", typed::TEXT]; + let mut offered = vec!["application/json", typed::TYPED_JSON, typed::TEXT]; if let Some(native) = native.filter(|native| *native != typed::TEXT) { offered.push(native); } @@ -461,7 +476,14 @@ fn shown(accept: Option<&HeaderValue>, kind: Kind) -> Result { fn show(held: &store::Held, shown: Shown, json: Option<&JsonValue>) -> Result { let json = || json.ok_or(ApiError(StatusCode::INTERNAL_SERVER_ERROR, None)); match shown { - Shown::Json(_) => respond(Some(held.kind), "application/json", json_bytes(json()?)?), + Shown::Json(form) => { + let media = if form.typed { + typed::TYPED_JSON + } else { + "application/json" + }; + respond(Some(held.kind), media, json_bytes(json()?)?) + } Shown::Text => { let body = match json()? { JsonValue::String(value) => value.clone().into_bytes(), @@ -508,64 +530,53 @@ fn respond(kind: Option, content_type: &str, body: Vec) -> Result (store::Held, Option) { - match self { - Written::Json(json) => { - let kind = match json { - JsonValue::Object(_) => Kind::Map, - JsonValue::Array(_) => Kind::List, - JsonValue::String(_) => Kind::String, - JsonValue::Bool(_) => Kind::Boolean, - JsonValue::Null => Kind::Null, - JsonValue::Number(number) if number.is_i64() => Kind::Int, - JsonValue::Number(number) if number.is_u64() => Kind::Uint, - JsonValue::Number(_) => Kind::Float, - }; - let value = store::json_scalar(json).ok().flatten(); - (store::Held { kind, value }, Some(json.clone())) - } - Written::Scalar(scalar) => ( - store::Held { - kind: Kind::of(scalar), - value: Some(scalar.clone()), - }, - typed::scalar_to_json(scalar, JsonForm::default()), - ), - } - } -} - -fn body_value(headers: &HeaderMap, body: &Bytes) -> Result { +/// A write's body as a value to write (`typed::Val`): JSON (the default), typed JSON, +/// text as a string, or a scalar of a type of its own (`typed::from_native`). +fn body_value(headers: &HeaderMap, body: &Bytes) -> Result { let content_type = headers .get(CONTENT_TYPE) .and_then(|value| value.to_str().ok()) .unwrap_or("application/json"); let (media, params) = content_type.split_once(';').unwrap_or((content_type, "")); let media = media.trim().to_ascii_lowercase(); + let bad = |error: String| ApiError(StatusCode::BAD_REQUEST, Some(error)); + let json = || { + serde_json::from_slice::(body).map_err(|error| bad(format!("not JSON: {error}"))) + }; match media.as_str() { - "application/json" => serde_json::from_slice::(body) - .map(Written::Json) - .map_err(|_| StatusCode::BAD_REQUEST.into()), + "application/json" => Val::from_json(&json()?).map_err(bad), + typed::TYPED_JSON => { + let form = json_form_of(params)?; + Val::from_typed_json(&json()?, form.timestamps).map_err(bad) + } typed::TEXT => String::from_utf8(body.to_vec()) - .map(|text| Written::Json(JsonValue::String(text))) - .map_err(|_| StatusCode::BAD_REQUEST.into()), + .map(|text| Val::Scalar(automerge::ScalarValue::Str(text.into()))) + .map_err(|_| bad("not UTF-8".to_owned())), media => match typed::from_native(media, params, body) { - Some(Ok(scalar)) => Ok(Written::Scalar(scalar)), - Some(Err(error)) => Err(ApiError(StatusCode::BAD_REQUEST, Some(error))), + Some(Ok(scalar)) => Ok(Val::Scalar(scalar)), + Some(Err(error)) => Err(bad(error)), None => Err(StatusCode::UNSUPPORTED_MEDIA_TYPE.into()), }, } } +/// What `value` is stored as, and its JSON of `shown`'s form, to answer its write with. +fn written(value: &Val, shown: Shown) -> (store::Held, Option) { + let held = store::Held { + kind: value.kind(), + value: match value { + Val::Scalar(scalar) => Some(scalar.clone()), + Val::Text(text) => Some(automerge::ScalarValue::Str(text.as_str().into())), + Val::Map(_) | Val::List(_) => None, + }, + }; + let form = match shown { + Shown::Json(form) => form, + Shown::Text | Shown::Native(_) => JsonForm::default(), + }; + (held, value.to_json(form)) +} + /// `POST /store/{key}`: inserts the body into a list, `key` ending in the index it /// goes to (`store::insert_path`: the items from there on move up; the length, or `-1`, /// appends); `201` with where it went (`{"index": n}`). Not a list there, or an index @@ -618,9 +629,8 @@ pub(crate) async fn post_store_entry( let index = path.last().and_then(|leaf| store::list_index(leaf)); let value = body_value(&headers, &body)?; let inserted = state - .mutate_doc_for(Some(user.0.clone()), move |doc| match &value { - Written::Json(value) => store::insert_path(doc, &path, value), - Written::Scalar(scalar) => store::insert_scalar_path(doc, &path, scalar), + .mutate_doc_for(Some(user.0.clone()), move |doc| { + store::insert_val_path(doc, &path, &value) }) .await; match inserted { -- cgit v1.3.1