Skip to main content

haystack_server/ops/
point_write.rs

1//! The `pointWrite` op — write a value to a writable point.
2
3use actix_web::{HttpRequest, HttpResponse, web};
4
5use haystack_core::data::{HDict, HGrid};
6use haystack_core::kinds::Kind;
7
8use crate::content;
9use crate::error::HaystackError;
10use crate::state::AppState;
11
12/// POST /api/pointWrite
13///
14/// Request grid has `id`, `level`, and `val` columns.
15/// Writes the value to the entity in the graph as a simple curVal update.
16pub async fn handle(
17    req: HttpRequest,
18    body: String,
19    state: web::Data<AppState>,
20) -> Result<HttpResponse, HaystackError> {
21    let content_type = req
22        .headers()
23        .get("Content-Type")
24        .and_then(|v| v.to_str().ok())
25        .unwrap_or("");
26    let accept = req
27        .headers()
28        .get("Accept")
29        .and_then(|v| v.to_str().ok())
30        .unwrap_or("");
31
32    let request_grid = content::decode_request_grid(&body, content_type)
33        .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
34
35    for row in request_grid.rows.iter() {
36        let ref_val = match row.get("id") {
37            Some(Kind::Ref(r)) => r.val.clone(),
38            _ => continue,
39        };
40
41        let level = match row.get("level") {
42            Some(Kind::Number(n)) => n.val as u32,
43            _ => 17, // Default level
44        };
45
46        if !(1..=17).contains(&level) {
47            return Err(HaystackError::bad_request(format!(
48                "level must be between 1 and 17, got {level}"
49            )));
50        }
51
52        // Check federation: if entity is not in local graph, proxy to remote.
53        if !state.graph.contains(&ref_val) {
54            if let Some(connector) = state.federation.owner_of(&ref_val) {
55                let val = row.get("val").cloned().unwrap_or(Kind::Null);
56                connector
57                    .proxy_point_write(&ref_val, level as u8, &val)
58                    .await
59                    .map_err(|e| HaystackError::internal(format!("federation proxy error: {e}")))?;
60                continue;
61            }
62            return Err(HaystackError::not_found(format!(
63                "entity not found: {ref_val}"
64            )));
65        }
66
67        // Check that the target entity has the `writable` marker tag
68        let entity = state
69            .graph
70            .get(&ref_val)
71            .ok_or_else(|| HaystackError::not_found(format!("entity not found: {ref_val}")))?;
72        if !entity.has("writable") {
73            return Err(HaystackError::bad_request(format!(
74                "entity '{ref_val}' is not writable"
75            )));
76        }
77
78        // Get the value to write
79        if let Some(val) = row.get("val") {
80            // Simple implementation: update the entity's curVal tag at the given level
81            let mut changes = HDict::new();
82            changes.set("curVal", val.clone());
83            changes.set(
84                "writeLevel",
85                Kind::Number(haystack_core::kinds::Number::unitless(level as f64)),
86            );
87            state
88                .graph
89                .update(&ref_val, changes)
90                .map_err(|e| HaystackError::bad_request(format!("write failed: {e}")))?;
91        }
92    }
93
94    // Return empty grid on success
95    let grid = HGrid::new();
96    let (encoded, ct) = content::encode_response_grid(&grid, accept)
97        .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
98
99    Ok(HttpResponse::Ok().content_type(ct).body(encoded))
100}