diff options
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/mod.rs | 2 | ||||
| -rw-r--r-- | src/api/store.rs | 76 |
2 files changed, 63 insertions, 15 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs index 3f60cd6..1f178e7 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -49,6 +49,7 @@ pub(crate) fn router( "/store/{*key}", get(store::get_store_entry) .put(store::put_store_entry) + .post(store::post_store_entry) .delete(store::delete_store_entry), ) .route("/stream", get(store::stream_store_root)) @@ -197,6 +198,7 @@ pub(crate) fn store_status(error: StoreError, context: &str) -> StatusCode { match error { StoreError::Conflict => StatusCode::CONFLICT, StoreError::NotFound => StatusCode::NOT_FOUND, + StoreError::NotAList | StoreError::PastTheEnd { .. } => StatusCode::CONFLICT, other => { error!(error = %format!("{other:#}"), "{context}"); StatusCode::INTERNAL_SERVER_ERROR diff --git a/src/api/store.rs b/src/api/store.rs index 7e8caed..89123e2 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -39,6 +39,13 @@ pub(crate) struct ReadQuery { } #[derive(Debug, Deserialize)] +pub(crate) struct InsertQuery { + /// Where in the list: the items from there on move up one. At the end without one, + /// or with `-1`. + index: Option<i64>, +} + +#[derive(Debug, Deserialize)] pub(crate) struct DeleteQuery { #[serde(default)] prune: bool, @@ -202,7 +209,19 @@ pub(crate) async fn put_store_entry( } let path = parse_write_path(&state, &user, &key, "PUT")?; + let value = body_value(&headers, &body)?; + + let written = value.clone(); + state + .mutate_doc(move |doc| store::write_path(doc, &path, &written)) + .await + .map_err(|e| store_status(e, "Failed to write store value"))?; + response_for_value(&value, headers.get(ACCEPT)) +} + +/// A write's body as a value: JSON (the default), or text as a string. +fn body_value(headers: &HeaderMap, body: &Bytes) -> Result<JsonValue, StatusCode> { let content_type = headers .get(CONTENT_TYPE) .and_then(|value| value.to_str().ok()) @@ -211,24 +230,51 @@ pub(crate) async fn put_store_entry( .next() .unwrap_or("") .trim(); - - let value = match content_type { + match content_type { "application/json" => { - serde_json::from_slice::<JsonValue>(&body).map_err(|_| StatusCode::BAD_REQUEST)? + serde_json::from_slice::<JsonValue>(body).map_err(|_| StatusCode::BAD_REQUEST) } - "text/plain" => JsonValue::String( - String::from_utf8(body.to_vec()).map_err(|_| StatusCode::BAD_REQUEST)?, - ), - _ => return Err(StatusCode::UNSUPPORTED_MEDIA_TYPE), - }; - - let written = value.clone(); - state - .mutate_doc(move |doc| store::write_path(doc, &path, &written)) - .await - .map_err(|e| store_status(e, "Failed to write store value"))?; + "text/plain" => String::from_utf8(body.to_vec()) + .map(JsonValue::String) + .map_err(|_| StatusCode::BAD_REQUEST), + _ => Err(StatusCode::UNSUPPORTED_MEDIA_TYPE), + } +} - response_for_value(&value, headers.get(ACCEPT)) +/// `POST /store/{key}`: inserts the body into the list at `key` (`store::insert_path`), +/// at `?index=` or at the end; `201` with where it went (`{"index": n}`). No list there, +/// or an index past its end: `409`, saying which. +pub(crate) async fn post_store_entry( + State(state): State<AppState>, + Extension(user): Extension<AuthenticatedUser>, + Path(key): Path<String>, + Query(query): Query<InsertQuery>, + headers: HeaderMap, + body: Bytes, +) -> Result<Response, StatusCode> { + if !state.is_ready().await { + return Err(StatusCode::SERVICE_UNAVAILABLE); + } + let path = parse_write_path(&state, &user, &key, "POST")?; + let index = match query.index { + None | Some(-1) => None, + Some(index) => Some(usize::try_from(index).map_err(|_| StatusCode::BAD_REQUEST)?), + }; + let value = body_value(&headers, &body)?; + let inserted = state + .mutate_doc(move |doc| store::insert_path(doc, &path, index, &value)) + .await; + match inserted { + Ok(index) => Ok(( + StatusCode::CREATED, + axum::Json(serde_json::json!({ "index": index })), + ) + .into_response()), + Err(error @ (StoreError::NotAList | StoreError::PastTheEnd { .. })) => { + Ok((StatusCode::CONFLICT, error.to_string()).into_response()) + } + Err(error) => Err(store_status(error, "Failed to insert into a list")), + } } /// `DELETE /store/{key}`. |
