Skip to main content

batuta/serve/banco/
handlers_recipes.rs

1//! Recipe endpoint handlers — create, list, get, run recipes.
2
3use axum::{extract::State, http::StatusCode, response::Json};
4use serde::Deserialize;
5
6use super::recipes::{DatasetResult, Recipe, RecipeStep};
7use super::state::BancoState;
8use super::types::ErrorResponse;
9
10/// POST /api/v1/data/recipes — create a recipe.
11pub async fn create_recipe_handler(
12    State(state): State<BancoState>,
13    Json(request): Json<CreateRecipeRequest>,
14) -> Json<Recipe> {
15    let recipe = state.recipes.create(
16        &request.name,
17        request.source_files,
18        request.steps,
19        &request.output_format.unwrap_or_else(|| "jsonl".to_string()),
20    );
21    Json(recipe)
22}
23
24/// GET /api/v1/data/recipes — list recipes.
25pub async fn list_recipes_handler(State(state): State<BancoState>) -> Json<RecipesListResponse> {
26    Json(RecipesListResponse { recipes: state.recipes.list() })
27}
28
29/// GET /api/v1/data/recipes/:id — get recipe by ID.
30pub async fn get_recipe_handler(
31    State(state): State<BancoState>,
32    axum::extract::Path(id): axum::extract::Path<String>,
33) -> Result<Json<Recipe>, (StatusCode, Json<ErrorResponse>)> {
34    state.recipes.get(&id).map(Json).ok_or((
35        StatusCode::NOT_FOUND,
36        Json(ErrorResponse::new(format!("Recipe {id} not found"), "not_found", 404)),
37    ))
38}
39
40/// POST /api/v1/data/recipes/:id/run — execute a recipe.
41pub async fn run_recipe_handler(
42    State(state): State<BancoState>,
43    axum::extract::Path(id): axum::extract::Path<String>,
44) -> Result<Json<DatasetResult>, (StatusCode, Json<ErrorResponse>)> {
45    let recipe = state.recipes.get(&id).ok_or((
46        StatusCode::NOT_FOUND,
47        Json(ErrorResponse::new(format!("Recipe {id} not found"), "not_found", 404)),
48    ))?;
49
50    // Gather source texts from uploaded files
51    let source_texts: Vec<(String, String)> = recipe
52        .source_files
53        .iter()
54        .filter_map(|file_id| {
55            let info = state.files.get(file_id)?;
56            // For in-memory store, read content; for disk, read from file
57            let content = state
58                .files
59                .read_content(file_id)
60                .map(|bytes| String::from_utf8_lossy(&bytes).to_string())
61                .unwrap_or_default();
62            Some((info.name, content))
63        })
64        .collect();
65
66    let source_refs: Vec<(&str, &str)> =
67        source_texts.iter().map(|(n, c)| (n.as_str(), c.as_str())).collect();
68
69    state.recipes.run(&id, &source_refs).map(Json).map_err(|e| {
70        (
71            StatusCode::INTERNAL_SERVER_ERROR,
72            Json(ErrorResponse::new(e.to_string(), "recipe_error", 500)),
73        )
74    })
75}
76
77/// GET /api/v1/data/datasets — list datasets.
78pub async fn list_datasets_handler(State(state): State<BancoState>) -> Json<DatasetsListResponse> {
79    Json(DatasetsListResponse { datasets: state.recipes.list_datasets() })
80}
81
82/// GET /api/v1/data/datasets/:id/preview — preview dataset rows.
83pub async fn preview_dataset_handler(
84    State(state): State<BancoState>,
85    axum::extract::Path(id): axum::extract::Path<String>,
86) -> Result<Json<DatasetPreview>, (StatusCode, Json<ErrorResponse>)> {
87    let dataset = state.recipes.get_dataset(&id).ok_or((
88        StatusCode::NOT_FOUND,
89        Json(ErrorResponse::new(format!("Dataset {id} not found"), "not_found", 404)),
90    ))?;
91
92    let preview_records: Vec<_> = dataset.records.iter().take(10).cloned().collect();
93    Ok(Json(DatasetPreview {
94        dataset_id: dataset.dataset_id,
95        total_records: dataset.record_count,
96        preview: preview_records,
97    }))
98}
99
100// ============================================================================
101// Request/Response types
102// ============================================================================
103
104#[derive(Debug, Deserialize)]
105pub struct CreateRecipeRequest {
106    pub name: String,
107    #[serde(default)]
108    pub source_files: Vec<String>,
109    pub steps: Vec<RecipeStep>,
110    #[serde(default)]
111    pub output_format: Option<String>,
112}
113
114#[derive(Debug, serde::Serialize)]
115pub struct RecipesListResponse {
116    pub recipes: Vec<Recipe>,
117}
118
119#[derive(Debug, serde::Serialize)]
120pub struct DatasetsListResponse {
121    pub datasets: Vec<DatasetResult>,
122}
123
124#[derive(Debug, serde::Serialize)]
125pub struct DatasetPreview {
126    pub dataset_id: String,
127    pub total_records: usize,
128    pub preview: Vec<super::recipes::Record>,
129}