haystack_server/ops/
his.rs1use actix_web::{HttpRequest, HttpResponse, web};
6use chrono::{DateTime, FixedOffset, Local, NaiveDate, NaiveTime, TimeZone};
7
8use haystack_core::data::{HCol, HDict, HGrid};
9use haystack_core::kinds::{HDateTime, HRef, Kind};
10
11use crate::content;
12use crate::error::HaystackError;
13use crate::his_store::HisItem;
14use crate::state::AppState;
15
16pub async fn handle_read(
29 req: HttpRequest,
30 body: String,
31 state: web::Data<AppState>,
32) -> Result<HttpResponse, HaystackError> {
33 let content_type = req
34 .headers()
35 .get("Content-Type")
36 .and_then(|v| v.to_str().ok())
37 .unwrap_or("");
38 let accept = req
39 .headers()
40 .get("Accept")
41 .and_then(|v| v.to_str().ok())
42 .unwrap_or("");
43
44 let request_grid = content::decode_request_grid(&body, content_type)
45 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
46
47 let row = request_grid
49 .row(0)
50 .ok_or_else(|| HaystackError::bad_request("hisRead request has no rows"))?;
51
52 let id = match row.get("id") {
54 Some(Kind::Ref(r)) => r.val.clone(),
55 _ => {
56 return Err(HaystackError::bad_request(
57 "hisRead: missing or invalid 'id' Ref",
58 ));
59 }
60 };
61
62 let range_str = match row.get("range") {
64 Some(Kind::Str(s)) => s.as_str(),
65 _ => {
66 return Err(HaystackError::bad_request(
67 "hisRead: missing or invalid 'range' Str",
68 ));
69 }
70 };
71
72 let (start, end) = parse_range(range_str)
74 .map_err(|e| HaystackError::bad_request(format!("hisRead: bad range: {e}")))?;
75
76 let items = state.his.read(&id, Some(start), Some(end));
78
79 let cols = vec![HCol::new("ts"), HCol::new("val")];
81 let rows: Vec<HDict> = items
82 .into_iter()
83 .map(|item| {
84 let mut d = HDict::new();
85 d.set("ts", Kind::DateTime(HDateTime::new(item.ts, "UTC")));
86 d.set("val", item.val);
87 d
88 })
89 .collect();
90
91 let mut meta = HDict::new();
92 meta.set("id", Kind::Ref(HRef::from_val(&id)));
93 let grid = HGrid::from_parts(meta, cols, rows);
94
95 log::info!("hisRead: returning {} rows for point {}", grid.len(), id);
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}
101
102fn parse_range(range: &str) -> Result<(DateTime<FixedOffset>, DateTime<FixedOffset>), String> {
110 let range = range.trim();
111
112 match range {
113 "today" => {
114 let today = Local::now().date_naive();
115 Ok(date_range(today, today))
116 }
117 "yesterday" => {
118 let yesterday = Local::now().date_naive() - chrono::Duration::days(1);
119 Ok(date_range(yesterday, yesterday))
120 }
121 _ => {
122 if range.contains(',') {
123 let parts: Vec<&str> = range.splitn(2, ',').collect();
124 let start_date = parse_date(parts[0].trim())?;
125 let end_date = parse_date(parts[1].trim())?;
126 Ok(date_range(start_date, end_date))
127 } else {
128 let date = parse_date(range)?;
129 Ok(date_range(date, date))
130 }
131 }
132 }
133}
134
135fn parse_date(s: &str) -> Result<NaiveDate, String> {
137 NaiveDate::parse_from_str(s, "%Y-%m-%d").map_err(|e| format!("invalid date '{s}': {e}"))
138}
139
140fn date_range(
144 start_date: NaiveDate,
145 end_date: NaiveDate,
146) -> (DateTime<FixedOffset>, DateTime<FixedOffset>) {
147 let utc = FixedOffset::east_opt(0).unwrap();
148 let start = utc
149 .from_local_datetime(&start_date.and_time(NaiveTime::MIN))
150 .unwrap();
151 let end = utc
152 .from_local_datetime(&end_date.and_hms_opt(23, 59, 59).unwrap())
153 .unwrap();
154 (start, end)
155}
156
157pub async fn handle_write(
166 req: HttpRequest,
167 body: String,
168 state: web::Data<AppState>,
169) -> Result<HttpResponse, HaystackError> {
170 let content_type = req
171 .headers()
172 .get("Content-Type")
173 .and_then(|v| v.to_str().ok())
174 .unwrap_or("");
175 let accept = req
176 .headers()
177 .get("Accept")
178 .and_then(|v| v.to_str().ok())
179 .unwrap_or("");
180
181 let request_grid = content::decode_request_grid(&body, content_type)
182 .map_err(|e| HaystackError::bad_request(format!("failed to decode request: {e}")))?;
183
184 let id = match request_grid.meta.get("id") {
186 Some(Kind::Ref(r)) => r.val.clone(),
187 _ => {
188 return Err(HaystackError::bad_request(
189 "hisWrite: grid meta must contain 'id' Ref",
190 ));
191 }
192 };
193
194 let mut items = Vec::with_capacity(request_grid.len());
196 for (i, row) in request_grid.iter().enumerate() {
197 let ts = match row.get("ts") {
198 Some(Kind::DateTime(hdt)) => hdt.dt,
199 _ => {
200 return Err(HaystackError::bad_request(format!(
201 "hisWrite: row {i} missing or invalid 'ts' DateTime"
202 )));
203 }
204 };
205 let val = row.get("val").cloned().unwrap_or(Kind::Null);
206
207 items.push(HisItem { ts, val });
208 }
209
210 let count = items.len();
211 state.his.write(&id, items);
212
213 log::info!("hisWrite: stored {} items for point {}", count, id);
214 let grid = HGrid::new();
215 let (encoded, ct) = content::encode_response_grid(&grid, accept)
216 .map_err(|e| HaystackError::internal(format!("encoding error: {e}")))?;
217
218 Ok(HttpResponse::Ok().content_type(ct).body(encoded))
219}