diff options
| author | tv <tv@krebsco.de> | 2026-11-02 10:26:51 +0100 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-02 10:26:51 +0100 |
| commit | 58d574166fef1246f367009546358703e16d7f7e (patch) | |
| tree | a307cd78560058c45e32f18631191e0fd15c9ae1 /src/api | |
| parent | 74ced20c6013b7ced9ae14a41033f60239790f6a (diff) | |
api: add application/vnd.continuum.typed+json
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/store.rs | 126 |
1 files changed, 68 insertions, 58 deletions
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<JsonForm, ApiError> { 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<JsonForm, ApiError> { ) })?, }; - 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<Shown, ApiError> { 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<Shown, ApiError> { _ => {} } } - 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<Shown, ApiError> { fn show(held: &store::Held, shown: Shown, json: Option<&JsonValue>) -> Result<Response, ApiError> { 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<Kind>, content_type: &str, body: Vec<u8>) -> Result<Resp /// Header naming what a value read or written is stored as (`typed::Kind::name`). const TYPE_HEADER: &str = "x-continuum-type"; -/// A write's body: JSON (the default), text as a string, or a value of a type of its -/// own (`typed::from_native`). -enum Written { - Json(JsonValue), - Scalar(automerge::ScalarValue), -} - -impl Written { - /// What it is stored as, and its JSON, to answer the write with. - fn held(&self) -> (store::Held, Option<JsonValue>) { - 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<Written, ApiError> { +/// 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<Val, ApiError> { 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::<JsonValue>(body).map_err(|error| bad(format!("not JSON: {error}"))) + }; match media.as_str() { - "application/json" => serde_json::from_slice::<JsonValue>(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<JsonValue>) { + 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 { |
