Files
pile/crates/pile-dataset/src/serve/schema.rs
2026-03-28 11:20:16 -07:00

131 lines
3.3 KiB
Rust

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<ApiValue>),
String(String),
Number(serde_json::Number),
Null,
}
pub type SchemaResponse = HashMap<String, ApiValue>;
async fn pile_value_to_api(
state: &ExtractState,
value: PileValue,
) -> Result<ApiValue, std::io::Error> {
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<Arc<Datasets>>,
Query(params): Query<SchemaQuery>,
) -> 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, &params.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()
}