Skip to main content

haystack_server/ops/
point_write.rs

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