use axum::{ Json, extract::{Path, Query, State}, http::{StatusCode, header}, response::{IntoResponse, Response}, }; use pile_config::Label; use pile_value::{extract::traits::ExtractState, value::PileValue}; use serde::Deserialize; use std::{sync::Arc, time::Instant}; use tracing::debug; use utoipa::IntoParams; use crate::Datasets; #[derive(Deserialize, IntoParams)] pub struct SchemaFieldQuery { source: String, key: String, #[serde(default)] download: bool, name: Option, } /// Extract a specific schema field from an item's metadata. #[utoipa::path( get, path = "/schema/{field}", params( ("field" = String, Path, description = "Schema field"), ("source" = String, Query, description = "Source label"), ("key" = String, Query, description = "Item key"), ("name" = Option, Query, description = "Downloaded filename; defaults to the last segment of the key"), ), responses( (status = 200, description = "Field value as JSON"), (status = 400, description = "Invalid source label or path"), (status = 404, description = "Item or field not found"), (status = 500, description = "Internal server error"), ) )] pub async fn schema_field( State(state): State>, Path(field): Path, Query(params): Query, ) -> Response { let start = Instant::now(); let label = match Label::try_from(params.source.clone()) { Ok(l) => l, Err(e) => return (StatusCode::BAD_REQUEST, format!("{e:?}")).into_response(), }; debug!( message = "Serving /schema/{field}", source = params.source, key = params.key, field = field, ); let Some(item) = state.get(&label, ¶ms.key).await else { return StatusCode::NOT_FOUND.into_response(); }; let field_label = match Label::new(&field) { Some(x) => x, None => return StatusCode::NOT_FOUND.into_response(), }; let paths = match state.config.schema.get(&field_label) { Some(x) => &x.path, None => return StatusCode::NOT_FOUND.into_response(), }; let extract_state = ExtractState { ignore_mime: false }; let item = PileValue::Item(item); 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 /schema/{field}", source = params.source, key = params.key, field = field, time_ms = start.elapsed().as_millis() ); let disposition_type = if params.download { "attachment" } else { "inline" }; let file_name = params.name.unwrap_or_else(|| { params .key .rsplit('/') .next() .unwrap_or(¶ms.key) .to_owned() }); let disposition = format!("{disposition_type}; filename=\"{file_name}\""); match value { PileValue::String(s) => ( StatusCode::OK, [ (header::CONTENT_TYPE, "text/plain".to_owned()), (header::CONTENT_DISPOSITION, disposition), ], s.to_string(), ) .into_response(), PileValue::Blob { mime, bytes } => ( StatusCode::OK, [ (header::CONTENT_TYPE, mime.to_string()), (header::CONTENT_DISPOSITION, disposition), ], bytes.as_ref().clone(), ) .into_response(), _ => match value.to_json(&extract_state).await { Ok(json) => ( StatusCode::OK, [(header::CONTENT_DISPOSITION, disposition)], Json(json), ) .into_response(), Err(e) => (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")).into_response(), }, } }