aboutsummaryrefslogtreecommitdiffstats
path: root/src/api
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-02 11:04:15 +0100
committertv <tv@krebsco.de>2026-11-02 11:04:15 +0100
commit2ad5d7c6ba0962e7ba2a8c919cddc1c2fa136f63 (patch)
treeddf869ba92e3d60f020d05a7a7f315267bc902d0 /src/api
parent11dabc03d8624557c146e0796dc4bc9c9c1050ee (diff)
api: add type casts
Diffstat (limited to 'src/api')
-rw-r--r--src/api/mod.rs3
-rw-r--r--src/api/store.rs50
2 files changed, 47 insertions, 6 deletions
diff --git a/src/api/mod.rs b/src/api/mod.rs
index ede1cfa..9bb2bed 100644
--- a/src/api/mod.rs
+++ b/src/api/mod.rs
@@ -212,7 +212,8 @@ pub(crate) fn store_status(error: StoreError, context: &str) -> ApiError {
| StoreError::NotAList
| StoreError::PastTheEnd { .. }
| StoreError::NotACounter
- | StoreError::Retyping(_) => StatusCode::CONFLICT,
+ | StoreError::Retyping(_)
+ | StoreError::NotCastable { .. } => StatusCode::CONFLICT,
StoreError::NotFound | StoreError::UnknownHeads => StatusCode::NOT_FOUND,
StoreError::Rejoining => StatusCode::SERVICE_UNAVAILABLE,
other => {
diff --git a/src/api/store.rs b/src/api/store.rs
index e6921cd..48bdafc 100644
--- a/src/api/store.rs
+++ b/src/api/store.rs
@@ -83,6 +83,12 @@ pub(crate) struct PostQuery {
/// Adds this to the counter at the path instead of inserting the body
/// (`store::increment_path`).
increment: Option<i64>,
+ /// Casts what is at the path to this type instead of inserting the body
+ /// (`store::cast_path`; `typed::Kind::name` names it).
+ cast: Option<String>,
+ /// With `cast`, how timestamps are read from and written as strings (`rfc3339` by
+ /// default; `typed::TimestampFormat`).
+ format: Option<String>,
}
#[derive(Debug, Deserialize)]
@@ -342,7 +348,11 @@ pub(crate) async fn put_store_entry(
.await?;
let read = read.ok_or(ApiError(StatusCode::INTERNAL_SERVER_ERROR, None))?;
- let mut response = with_heads(read.respond()?, &heads);
+ Ok(with_retyped(with_heads(read.respond()?, &heads), &retyped))
+}
+
+/// `response` saying where a write changed types (`RETYPED_HEADER`), if it did.
+fn with_retyped(mut response: Response, retyped: &[store::Retyped]) -> Response {
if !retyped.is_empty() {
let mut listed: Vec<String> = retyped
.iter()
@@ -356,7 +366,7 @@ pub(crate) async fn put_store_entry(
response.headers_mut().insert(RETYPED_HEADER, value);
}
}
- Ok(response)
+ response
}
fn etag_for(bytes: &[u8]) -> String {
@@ -619,7 +629,9 @@ fn body_value(headers: &HeaderMap, body: &Bytes) -> Result<WriteBody, ApiError>
/// appends); `201` with where it went (`{"index": n}`). Not a list there, or an index
/// past its end: `409`, saying which. With `?increment=n`, adds `n` to the counter at
/// `key` instead (`store::increment_path`), answering with its value then; not a
-/// counter there: `409`.
+/// counter there: `409`. With `?cast=<type>`, casts what is at `key` to that type
+/// instead (`store::cast_path`), answering with it then, as `PUT` does; no cast
+/// possible: `409`.
pub(crate) async fn post_store_entry(
State(state): State<AppState>,
Extension(user): Extension<AuthenticatedUser>,
@@ -633,8 +645,15 @@ pub(crate) async fn post_store_entry(
return Err(StatusCode::SERVICE_UNAVAILABLE.into());
}
let path = parse_write_path(&state, &user, &key, "POST")?;
- // An increment names an item (`-1`: the last); an insert where it goes (`-1`: the end).
- let insertion = query.increment.is_none();
+ if query.increment.is_some() && query.cast.is_some() {
+ return Err(ApiError(
+ StatusCode::BAD_REQUEST,
+ Some("increment or cast, not both".to_owned()),
+ ));
+ }
+ // An increment or a cast names an item (`-1`: the last); an insert where it goes
+ // (`-1`: the end).
+ let insertion = query.increment.is_none() && query.cast.is_none();
if let Some(redirect) =
to_positive(&state, &path, insertion, None, raw_query.as_deref()).await?
{
@@ -663,6 +682,27 @@ pub(crate) async fn post_store_entry(
&heads,
));
}
+ if let Some(name) = &query.cast {
+ let bad = |message: String| ApiError(StatusCode::BAD_REQUEST, Some(message));
+ let to = Kind::named(name).ok_or_else(|| bad(format!("no type {name:?}")))?;
+ let timestamps = match &query.format {
+ None => TimestampFormat::default(),
+ Some(format) => TimestampFormat::named(format)
+ .ok_or_else(|| bad(format!("no timestamp format {format:?}")))?,
+ };
+ let accept = headers.get(ACCEPT).cloned();
+ // A value that can't be shown as asked is refused, and the cast with it.
+ let ((retyped, read), heads) = state
+ .mutate_doc_for(Some(user.0.clone()), move |doc| {
+ let retyped = store::cast_path(doc, &path, to, timestamps)
+ .map_err(|e| store_status(e, "Failed to cast a store value"))?;
+ let read = read_shown(&*doc, &path, accept.as_ref(), false)?;
+ Ok::<_, ApiError>((retyped, read))
+ })
+ .await?;
+ let read = read.ok_or(ApiError(StatusCode::INTERNAL_SERVER_ERROR, None))?;
+ return Ok(with_retyped(with_heads(read.respond()?, &heads), &retyped));
+ }
let index = path.last().and_then(|leaf| store::list_index(leaf));
let value = body_value(&headers, &body)?;
let inserted = state