Skip to main content

haystack_server/ops/
read.rs

1//! The `read` op — read entities by filter or by id.
2
3use axum::extract::State;
4use axum::http::HeaderMap;
5use axum::response::{IntoResponse, Response};
6
7use haystack_core::data::{HCol, HDict, HGrid};
8use haystack_core::kinds::{HRef, Kind};
9use std::sync::Arc;
10
11use crate::content;
12use crate::error::HaystackError;
13use crate::state::SharedState;
14
15/// Helper to build an Axum response from encoded grid output.
16fn haystack_response(body: Vec<u8>, content_type: &str) -> Response {
17    (
18        [(axum::http::header::CONTENT_TYPE, content_type.to_string())],
19        body,
20    )
21        .into_response()
22}
23
24/// POST /api/read
25pub async fn handle(
26    State(state): State<SharedState>,
27    headers: HeaderMap,
28    body: String,
29) -> Result<Response, HaystackError> {
30    let content_type = headers
31        .get("Content-Type")
32        .and_then(|v| v.to_str().ok())
33        .unwrap_or("");
34    let accept = headers
35        .get("Accept")
36        .and_then(|v| v.to_str().ok())
37        .unwrap_or("");
38
39    let request_grid = content::decode_request_grid(&body, content_type)
40        .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
41
42    let result_grid = if request_grid.col("id").is_some() {
43        read_by_id(&request_grid, &state)
44    } else if request_grid.col("filter").is_some() {
45        read_by_filter(&request_grid, &state)
46    } else {
47        Err(HaystackError::bad_request(
48            "request must have 'id' or 'filter' column",
49        ))
50    }?;
51
52    let (encoded, ct) = content::encode_response_grid(&result_grid, accept)
53        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
54
55    Ok(haystack_response(encoded, ct))
56}
57
58/// Read entities by filter expression.
59fn read_by_filter(request_grid: &HGrid, state: &SharedState) -> Result<HGrid, HaystackError> {
60    let row = request_grid
61        .row(0)
62        .ok_or_else(|| HaystackError::bad_request("request grid has no rows"))?;
63
64    let filter = match row.get("filter") {
65        Some(Kind::Str(s)) => s.as_str(),
66        _ => return Err(HaystackError::bad_request("filter must be a Str value")),
67    };
68
69    let limit = match row.get("limit") {
70        Some(Kind::Number(n)) => n.val as usize,
71        _ => 0, // 0 means no limit
72    };
73
74    let effective_limit = if limit == 0 { usize::MAX } else { limit };
75    let is_wildcard = filter == "*";
76
77    let mut results: Vec<Arc<HDict>> = if is_wildcard {
78        state
79            .graph
80            .all_entities()
81            .into_iter()
82            .map(Arc::new)
83            .collect()
84    } else {
85        let local_grid = state
86            .graph
87            .read_filter(filter, limit)
88            .map_err(|e| HaystackError::bad_request(format!("filter error: {e}")))?;
89        local_grid.rows.into_iter().map(Arc::new).collect()
90    };
91
92    if results.len() > effective_limit {
93        results.truncate(effective_limit);
94    }
95
96    if results.is_empty() {
97        return Ok(HGrid::new());
98    }
99
100    // Build column set from all result entities.
101    let mut col_set: Vec<&str> = Vec::new();
102    let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
103    for entity in &results {
104        for name in entity.tag_names() {
105            if seen.insert(name) {
106                col_set.push(name);
107            }
108        }
109    }
110    col_set.sort_unstable();
111    let cols: Vec<HCol> = col_set.iter().map(|&n| HCol::new(n)).collect();
112
113    Ok(HGrid::from_parts_arc(HDict::new(), cols, results))
114}
115
116/// Read entities by ID references.
117fn read_by_id(request_grid: &HGrid, state: &SharedState) -> Result<HGrid, HaystackError> {
118    let mut results: Vec<Arc<HDict>> = Vec::new();
119
120    for row in request_grid.rows.iter() {
121        let ref_val = match row.get("id") {
122            Some(Kind::Ref(r)) => &r.val,
123            _ => continue,
124        };
125
126        if let Some(entity) = state.graph.get(ref_val) {
127            results.push(Arc::new(entity));
128        } else {
129            // Add missing stub for IDs not found.
130            let mut missing = HDict::new();
131            missing.set("id", Kind::Ref(HRef::from_val(ref_val.as_str())));
132            results.push(Arc::new(missing));
133        }
134    }
135
136    if results.is_empty() {
137        return Ok(HGrid::new());
138    }
139
140    // Build column set.
141    let mut col_set: Vec<&str> = Vec::new();
142    let mut seen: std::collections::HashSet<&str> = std::collections::HashSet::new();
143    for entity in &results {
144        for name in entity.tag_names() {
145            if seen.insert(name) {
146                col_set.push(name);
147            }
148        }
149    }
150
151    col_set.sort_unstable();
152    let cols: Vec<HCol> = col_set.iter().map(|&n| HCol::new(n)).collect();
153    Ok(HGrid::from_parts_arc(HDict::new(), cols, results))
154}