1use std::future::Future;
12use std::pin::Pin;
13use std::sync::Arc;
14
15use tokio::sync::{Mutex, mpsc};
16
17use spvirit_codec::spvd_decode::DecodedValue;
18use spvirit_types::{NtPayload, NtScalar, NtScalarArray, ScalarArrayValue};
19
20use crate::pvstore::{PvInfo, Source};
21use crate::simple_store::{SimplePvStore, descriptor_for_payload};
22use crate::types::{RecordInstance, RecordType, ScalarValue};
23
24#[derive(Debug, Clone, PartialEq, Eq)]
26pub struct FieldRef {
27 pub base: String,
28 pub field: String,
29 pub long_string: bool,
31}
32
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
35pub enum FieldKind {
36 Str,
37 Int,
38 Double,
39}
40
41pub fn parse_field_ref(name: &str) -> Option<FieldRef> {
48 let (base, field_part) = name.rsplit_once('.')?;
49 if base.is_empty() {
50 return None;
51 }
52 let (field, long_string) = match field_part.strip_suffix('$') {
53 Some(stripped) => (stripped, true),
54 None => (field_part, false),
55 };
56 if field.is_empty()
57 || !field
58 .bytes()
59 .all(|b| b.is_ascii_uppercase() || b.is_ascii_digit())
60 {
61 return None;
62 }
63 Some(FieldRef {
64 base: base.to_string(),
65 field: field.to_string(),
66 long_string,
67 })
68}
69
70const DBCOMMON_DEFAULTS: &[(&str, FieldKind, &str)] = &[
76 ("DESC", FieldKind::Str, ""),
77 ("SCAN", FieldKind::Str, "Passive"),
78 ("PINI", FieldKind::Str, "NO"),
79 ("PHAS", FieldKind::Int, "0"),
80 ("EVNT", FieldKind::Str, ""),
81 ("PRIO", FieldKind::Str, "LOW"),
82 ("DISV", FieldKind::Int, "1"),
83 ("DISA", FieldKind::Int, "0"),
84 ("SDIS", FieldKind::Str, ""),
85 ("DISS", FieldKind::Str, "NO_ALARM"),
86 ("PROC", FieldKind::Int, "0"),
87 ("STAT", FieldKind::Str, "UDF"),
88 ("SEVR", FieldKind::Str, "INVALID"),
89 ("UDF", FieldKind::Int, "1"),
90 ("TPRO", FieldKind::Int, "0"),
91 ("FLNK", FieldKind::Str, ""),
92 ("ADEL", FieldKind::Double, "0"),
93 ("MDEL", FieldKind::Double, "0"),
94 ("TSE", FieldKind::Int, "0"),
95 ("DISP", FieldKind::Int, "0"),
96 ("ACKS", FieldKind::Str, "NO_ALARM"),
97 ("ACKT", FieldKind::Str, "YES"),
98 ("ASG", FieldKind::Str, ""),
99];
100
101pub fn dbcommon_default(field: &str) -> Option<(FieldKind, &'static str)> {
103 DBCOMMON_DEFAULTS
104 .iter()
105 .find(|(name, _, _)| *name == field)
106 .map(|(_, kind, default)| (*kind, *default))
107}
108
109pub fn record_type_name(rt: &RecordType) -> &'static str {
111 match rt {
112 RecordType::Ai => "ai",
113 RecordType::Ao => "ao",
114 RecordType::Bi => "bi",
115 RecordType::Bo => "bo",
116 RecordType::StringIn => "stringin",
117 RecordType::StringOut => "stringout",
118 RecordType::Waveform => "waveform",
119 RecordType::Aai => "aai",
120 RecordType::Aao => "aao",
121 RecordType::SubArray => "subArray",
122 RecordType::NtTable => "ntTable",
123 RecordType::NtNdArray => "ntNDArray",
124 RecordType::Mbbi => "mbbi",
125 RecordType::Mbbo => "mbbo",
126 RecordType::Generic => "generic",
127 }
128}
129
130pub fn mdel_of(record: &RecordInstance) -> f64 {
132 record
133 .raw_fields
134 .get("MDEL")
135 .and_then(|s| s.trim().parse::<f64>().ok())
136 .unwrap_or(0.0)
137}
138
139fn typed_value(kind: FieldKind, raw: &str) -> ScalarValue {
141 match kind {
142 FieldKind::Int => raw
143 .trim()
144 .parse::<i32>()
145 .map(ScalarValue::I32)
146 .unwrap_or_else(|_| ScalarValue::Str(raw.to_string())),
147 FieldKind::Double => raw
148 .trim()
149 .parse::<f64>()
150 .map(ScalarValue::F64)
151 .unwrap_or_else(|_| ScalarValue::Str(raw.to_string())),
152 FieldKind::Str => ScalarValue::Str(raw.to_string()),
153 }
154}
155
156pub fn field_value(record: &RecordInstance, field: &str) -> Option<ScalarValue> {
162 match field {
163 "RTYP" => {
164 return Some(ScalarValue::Str(
165 record_type_name(&record.record_type).to_string(),
166 ));
167 }
168 "NAME" => return Some(ScalarValue::Str(record.name.clone())),
169 "VAL" => return Some(record.current_value()),
170 "DTYP" => {
171 let dtyp = record
172 .raw_fields
173 .get("DTYP")
174 .cloned()
175 .unwrap_or_else(|| "Soft Channel".to_string());
176 return Some(ScalarValue::Str(dtyp));
177 }
178 _ => {}
179 }
180
181 let kind = dbcommon_default(field).map(|(kind, _)| kind);
182 if let Some(raw) = record.raw_fields.get(field) {
183 return Some(typed_value(kind.unwrap_or(FieldKind::Str), raw));
184 }
185 if field == "DESC" {
186 return Some(ScalarValue::Str(record.common.desc.clone()));
187 }
188 dbcommon_default(field).map(|(kind, default)| typed_value(kind, default))
189}
190
191pub fn payload_for(record: &RecordInstance, field_ref: &FieldRef) -> Option<NtPayload> {
197 let value = field_value(record, &field_ref.field)?;
198 if field_ref.long_string {
199 let ScalarValue::Str(s) = value else {
200 return None;
201 };
202 let bytes: Vec<i8> = s.into_bytes().into_iter().map(|b| b as i8).collect();
203 return Some(NtPayload::ScalarArray(NtScalarArray::from_value(
204 ScalarArrayValue::I8(bytes),
205 )));
206 }
207 let mut nt = NtScalar::from_value(value);
208 nt.display_description = record.common.desc.clone();
209 Some(NtPayload::Scalar(nt))
210}
211
212pub struct RecordFieldSource {
218 store: Arc<SimplePvStore>,
219 open_subs: Mutex<Vec<mpsc::Sender<NtPayload>>>,
223}
224
225impl RecordFieldSource {
226 pub fn new(store: Arc<SimplePvStore>) -> Self {
227 Self {
228 store,
229 open_subs: Mutex::new(Vec::new()),
230 }
231 }
232
233 async fn resolve(&self, name: &str) -> Option<NtPayload> {
234 let field_ref = parse_field_ref(name)?;
235 let record = self.store.get_record(&field_ref.base).await?;
236 payload_for(&record, &field_ref)
237 }
238}
239
240impl Source for RecordFieldSource {
241 fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>> {
242 let name = name.to_string();
243 Box::pin(async move {
244 let payload = self.resolve(&name).await?;
245 Some(PvInfo {
246 descriptor: descriptor_for_payload(&payload),
247 writable: false,
248 })
249 })
250 }
251
252 fn get(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + '_>> {
253 let name = name.to_string();
254 Box::pin(async move { self.resolve(&name).await })
255 }
256
257 fn put(
258 &self,
259 name: &str,
260 _value: &DecodedValue,
261 ) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + '_>> {
262 let name = name.to_string();
263 Box::pin(async move { Err(format!("field PV '{}' is read-only", name)) })
264 }
265
266 fn subscribe(
267 &self,
268 name: &str,
269 ) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + '_>> {
270 let name = name.to_string();
271 Box::pin(async move {
272 let initial = self.resolve(&name).await?;
273 let (tx, rx) = mpsc::channel(4);
274 let _ = tx.try_send(initial);
275 self.open_subs.lock().await.push(tx);
276 Some(rx)
277 })
278 }
279
280 fn names(&self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + '_>> {
281 Box::pin(async move { Vec::new() })
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290
291 #[test]
292 fn parses_simple_field_ref() {
293 let r = parse_field_ref("SIM:AO.RTYP").unwrap();
294 assert_eq!(
295 (r.base.as_str(), r.field.as_str(), r.long_string),
296 ("SIM:AO", "RTYP", false)
297 );
298 }
299
300 #[test]
301 fn parses_long_string_suffix() {
302 let r = parse_field_ref("SIM:AO.DESC$").unwrap();
303 assert_eq!((r.field.as_str(), r.long_string), ("DESC", true));
304 }
305
306 #[test]
307 fn rejects_non_field_names() {
308 assert!(parse_field_ref("SIM:AO").is_none()); assert!(parse_field_ref("SIM:AO.").is_none()); assert!(parse_field_ref("SIM:AO.rtyp").is_none()); assert!(parse_field_ref("SIM:AO.$").is_none()); assert!(parse_field_ref(".RTYP").is_none()); }
314
315 #[test]
316 fn dbcommon_defaults_cover_key_fields() {
317 assert!(matches!(
318 dbcommon_default("SCAN"),
319 Some((FieldKind::Str, "Passive"))
320 ));
321 assert!(matches!(
322 dbcommon_default("PINI"),
323 Some((FieldKind::Str, "NO"))
324 ));
325 assert!(matches!(
326 dbcommon_default("PHAS"),
327 Some((FieldKind::Int, "0"))
328 ));
329 assert!(matches!(
330 dbcommon_default("MDEL"),
331 Some((FieldKind::Double, "0"))
332 ));
333 assert!(dbcommon_default("NOTAFIELD").is_none());
334 }
335
336 #[test]
337 fn record_type_names_match_db_names() {
338 assert_eq!(record_type_name(&RecordType::Ao), "ao");
339 assert_eq!(record_type_name(&RecordType::StringIn), "stringin");
340 }
341
342 fn test_record() -> RecordInstance {
343 let recs = crate::db::parse_db(
344 r#"
345record(ao, "SIM:AO") {
346 field(VAL, "2.34")
347 field(DESC, "A test output")
348 field(EGU, "V")
349 field(MDEL, "0.5")
350}"#,
351 )
352 .expect("parse");
353 recs.get("SIM:AO").expect("record present").clone()
354 }
355
356 #[test]
357 fn computed_fields_resolve() {
358 let r = test_record();
359 assert_eq!(field_value(&r, "RTYP"), Some(ScalarValue::Str("ao".into())));
360 assert_eq!(
361 field_value(&r, "NAME"),
362 Some(ScalarValue::Str("SIM:AO".into()))
363 );
364 assert_eq!(
365 field_value(&r, "DTYP"),
366 Some(ScalarValue::Str("Soft Channel".into()))
367 );
368 assert_eq!(field_value(&r, "VAL"), Some(ScalarValue::F64(2.34)));
369 }
370
371 #[test]
372 fn raw_db_fields_take_precedence_over_defaults() {
373 let r = test_record();
374 assert_eq!(
375 field_value(&r, "DESC"),
376 Some(ScalarValue::Str("A test output".into()))
377 );
378 assert_eq!(field_value(&r, "EGU"), Some(ScalarValue::Str("V".into())));
379 assert_eq!(field_value(&r, "MDEL"), Some(ScalarValue::F64(0.5)));
380 }
381
382 #[test]
383 fn dbcommon_defaults_fill_absent_fields() {
384 let r = test_record();
385 assert_eq!(
386 field_value(&r, "SCAN"),
387 Some(ScalarValue::Str("Passive".into()))
388 );
389 assert_eq!(field_value(&r, "PHAS"), Some(ScalarValue::I32(0)));
390 assert_eq!(field_value(&r, "ADEL"), Some(ScalarValue::F64(0.0)));
391 assert_eq!(field_value(&r, "NOTAFIELD"), None);
392 }
393
394 fn test_source() -> RecordFieldSource {
395 let recs = crate::db::parse_db(
396 r#"
397record(ao, "SIM:AO") {
398 field(VAL, "2.34")
399 field(DESC, "A test output")
400 field(MDEL, "0.5")
401}"#,
402 )
403 .expect("parse");
404 let store = SimplePvStore::new(recs, std::collections::HashMap::new(), Vec::new(), false);
405 RecordFieldSource::new(Arc::new(store))
406 }
407
408 #[tokio::test]
409 async fn claims_field_pvs_read_only() {
410 let src = test_source();
411 let info = src.claim("SIM:AO.RTYP").await.expect("claimed");
412 assert!(!info.writable);
413 match src.get("SIM:AO.RTYP").await.expect("payload") {
414 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::Str("ao".into())),
415 other => panic!("expected scalar, got {other:?}"),
416 }
417 }
418
419 #[tokio::test]
420 async fn does_not_claim_non_field_names() {
421 let src = test_source();
422 assert!(src.claim("SIM:AO").await.is_none()); assert!(src.claim("SIM:AO.NOTAFIELD").await.is_none());
424 assert!(src.claim("SIM:MISSING.RTYP").await.is_none());
425 assert!(src.claim("SIM:AO.MDEL$").await.is_none()); }
427
428 #[tokio::test]
429 async fn put_is_rejected() {
430 let src = test_source();
431 let err = src
432 .put("SIM:AO.DESC", &DecodedValue::Int32(1))
433 .await
434 .expect_err("put must fail");
435 assert!(err.contains("read-only"), "unexpected error: {err}");
436 }
437
438 #[tokio::test]
439 async fn long_string_serves_utf8_bytes() {
440 let src = test_source();
441 match src.get("SIM:AO.DESC$").await.expect("payload") {
442 NtPayload::ScalarArray(arr) => {
443 let ScalarArrayValue::I8(bytes) = arr.value else {
444 panic!("expected Int8 array, got {:?}", arr.value);
445 };
446 let expected: Vec<i8> = "A test output".bytes().map(|b| b as i8).collect();
447 assert_eq!(bytes, expected);
448 }
449 other => panic!("expected scalar array, got {other:?}"),
450 }
451 }
452
453 #[tokio::test]
454 async fn subscribe_delivers_initial_snapshot() {
455 let src = test_source();
456 let mut rx = src.subscribe("SIM:AO.RTYP").await.expect("subscribed");
457 match rx.recv().await.expect("initial value") {
458 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::Str("ao".into())),
459 other => panic!("expected scalar, got {other:?}"),
460 }
461 assert!(rx.try_recv().is_err());
463 }
464}