aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-10-30 20:34:47 +0100
committertv <tv@krebsco.de>2026-10-30 20:34:47 +0100
commita29a38db0ad7fc8d42a533145c8a489d97702fb5 (patch)
treeae1689804484d2aceabf106bb82cd4f0fcf97b36 /src/api
parentcaf212f916ce8fb31d7dba8bbe6d537092d324d5 (diff)
add support to insert values at list index
Diffstat (limited to 'src/api')
-rw-r--r--src/api/mod.rs2
-rw-r--r--src/api/store.rs76
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}`.