use axum::{ Json, extract::{Query, State}, http::StatusCode, response::{IntoResponse, Response}, }; use pile_config::Label; use pile_value::{extract::traits::ExtractState, value::PileValue}; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, sync::Arc}; use utoipa::IntoParams; use crate::Datasets; #[derive(Deserialize, IntoParams)] pub struct SchemaQuery { source: String, key: String, #[serde(default)] hidden: bool, } #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum ApiValue { Binary { binary: bool, mime: String }, Object { object: bool }, Array(Vec), String(String), Number(serde_json::Number), Null, } pub type SchemaResponse = HashMap; async fn pile_value_to_api( state: &ExtractState, value: PileValue, ) -> Result { match value { PileValue::String(s) => Ok(ApiValue::String(s.to_string())), PileValue::U64(n) => Ok(ApiValue::Number(n.into())), PileValue::I64(n) => Ok(ApiValue::Number(n.into())), PileValue::Null => Ok(ApiValue::Null), PileValue::Binary(x) => Ok(ApiValue::Binary { binary: true, mime: x.mime().to_string(), }), PileValue::Array(arr) => { let mut out = Vec::with_capacity(arr.len()); for item in arr.iter() { out.push(Box::pin(pile_value_to_api(state, item.clone())).await?); } Ok(ApiValue::Array(out)) } PileValue::ObjectExtractor(_) | PileValue::ListExtractor(_) | PileValue::Item(_) => { Ok(ApiValue::Object { object: true }) } } } /// Get all schema field values for a single item. #[utoipa::path( get, path = "/schema", params( ("source" = String, Query, description = "Source label"), ("key" = String, Query, description = "Item key"), ("hidden" = bool, Query, description = "Include hidden fields (default: false)"), ), responses( (status = 200, description = "Schema field values as a map of label to value"), (status = 400, description = "Invalid source label"), (status = 404, description = "Item not found"), (status = 500, description = "Internal server error"), ) )] pub async fn schema_all( State(state): State>, Query(params): Query, ) -> Response { let label = match Label::try_from(params.source.clone()) { Ok(l) => l, Err(e) => return (StatusCode::BAD_REQUEST, format!("{e:?}")).into_response(), }; let Some(item) = state.get(&label, ¶ms.key).await else { return StatusCode::NOT_FOUND.into_response(); }; let extract_state = ExtractState { ignore_mime: false }; let item = PileValue::Item(item); let mut result: SchemaResponse = HashMap::new(); for (field_label, field_spec) in &state.config.schema { if field_spec.hidden && !params.hidden { continue; } let mut value = None; for path in &field_spec.path { 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(v) = value else { continue }; let api_value = match pile_value_to_api(&extract_state, v).await { Ok(v) => v, Err(e) => return (StatusCode::INTERNAL_SERVER_ERROR, format!("{e:?}")).into_response(), }; result.insert(field_label.as_str().to_owned(), api_value); } (StatusCode::OK, Json(result)).into_response() }