Many field paths
Some checks failed
CI / Typos (push) Successful in 22s
CI / Build and test (push) Failing after 2m40s
CI / Clippy (push) Successful in 3m28s
CI / Build and test (all features) (push) Failing after 11m10s

This commit is contained in:
2026-03-23 22:41:07 -07:00
parent d95ebeaba0
commit 6f267880c8

View File

@@ -1,6 +1,6 @@
use axum::{
Json,
extract::{Query, State},
extract::{Query, RawQuery, State},
http::{StatusCode, header},
response::{IntoResponse, Response},
};
@@ -17,19 +17,19 @@ use crate::Datasets;
pub struct FieldQuery {
source: String,
key: String,
path: String,
#[serde(default)]
download: bool,
}
/// Extract a specific field from an item's metadata
/// Extract a specific field from an item's metadata.
/// Multiple `path` parameters may be provided; the first non-null result is returned.
#[utoipa::path(
get,
path = "/field",
params(
("source" = String, Query, description = "Source label"),
("key" = String, Query, description = "Item key"),
("path" = String, Query, description = "Object path (e.g. $.flac.title)"),
("path" = String, Query, description = "Object path (e.g. $.flac.title); repeat for fallbacks"),
),
responses(
(status = 200, description = "Field value as JSON"),
@@ -41,43 +41,73 @@ pub struct FieldQuery {
pub async fn get_field(
State(state): State<Arc<Datasets>>,
Query(params): Query<FieldQuery>,
RawQuery(raw_query): RawQuery,
) -> Response {
let start = Instant::now();
debug!(
message = "Serving /field",
source = params.source,
key = params.key,
path = params.path,
);
let label = match Label::try_from(params.source.clone()) {
Ok(l) => l,
Err(e) => return (StatusCode::BAD_REQUEST, format!("{e:?}")).into_response(),
};
let path: ObjectPath = match params.path.parse() {
Ok(p) => p,
Err(e) => return (StatusCode::BAD_REQUEST, format!("{e:?}")).into_response(),
// Collect all `path` query params in order (supports repeated ?path=...&path=...)
let raw = raw_query.as_deref().unwrap_or("");
let paths: Vec<ObjectPath> = {
let mut result = Vec::new();
for part in raw.split('&') {
if let Some((k, v)) = part.split_once('=')
&& k == "path"
{
match v.parse::<ObjectPath>() {
Ok(p) => result.push(p),
Err(e) => {
return (StatusCode::BAD_REQUEST, format!("{e:?}")).into_response();
}
}
}
}
result
};
if paths.is_empty() {
return (StatusCode::BAD_REQUEST, "Missing `path` query parameter").into_response();
}
debug!(
message = "Serving /field",
source = params.source,
key = params.key,
paths = paths.len(),
);
let Some(item) = state.get(&label, &params.key).await else {
return StatusCode::NOT_FOUND.into_response();
};
let state = ExtractState { ignore_mime: false };
let extract_state = ExtractState { ignore_mime: false };
let item = PileValue::Item(item);
let value = match item.query(&state, &path).await {
Ok(Some(v)) => v,
Ok(None) => return StatusCode::NOT_FOUND.into_response(),
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")).into_response(),
// Try each path in order, returning the first non-null result
let mut value = None;
for path in &paths {
match item.query(&extract_state, path).await {
Ok(Some(PileValue::Null)) | Ok(None) => continue,
Ok(Some(v)) => {
value = Some(v);
break;
}
Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")).into_response(),
}
}
let Some(value) = value else {
return StatusCode::NOT_FOUND.into_response();
};
debug!(
message = "Served /field",
source = params.source,
key = params.key,
path = params.path,
time_ms = start.elapsed().as_millis()
);
@@ -106,7 +136,7 @@ pub async fn get_field(
bytes.as_ref().clone(),
)
.into_response(),
_ => match value.to_json(&state).await {
_ => match value.to_json(&extract_state).await {
Ok(json) => (
StatusCode::OK,
[(header::CONTENT_DISPOSITION, disposition.to_owned())],