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 RecordType::LongIn => "longin",
128 RecordType::LongOut => "longout",
129 }
130}
131
132pub fn mdel_of(record: &RecordInstance) -> f64 {
134 record
135 .raw_fields
136 .get("MDEL")
137 .and_then(|s| s.trim().parse::<f64>().ok())
138 .unwrap_or(0.0)
139}
140
141fn typed_value(kind: FieldKind, raw: &str) -> ScalarValue {
143 match kind {
144 FieldKind::Int => raw
145 .trim()
146 .parse::<i32>()
147 .map(ScalarValue::I32)
148 .unwrap_or_else(|_| ScalarValue::Str(raw.to_string())),
149 FieldKind::Double => raw
150 .trim()
151 .parse::<f64>()
152 .map(ScalarValue::F64)
153 .unwrap_or_else(|_| ScalarValue::Str(raw.to_string())),
154 FieldKind::Str => ScalarValue::Str(raw.to_string()),
155 }
156}
157
158pub fn field_value(record: &RecordInstance, field: &str) -> Option<ScalarValue> {
164 match field {
165 "RTYP" => {
166 return Some(ScalarValue::Str(
167 record_type_name(&record.record_type).to_string(),
168 ));
169 }
170 "NAME" => return Some(ScalarValue::Str(record.name.clone())),
171 "VAL" => return Some(record.current_value()),
172 "DTYP" => {
173 let dtyp = record
174 .raw_fields
175 .get("DTYP")
176 .cloned()
177 .unwrap_or_else(|| "Soft Channel".to_string());
178 return Some(ScalarValue::Str(dtyp));
179 }
180 _ => {}
181 }
182
183 let kind = dbcommon_default(field).map(|(kind, _)| kind);
184 if let Some(raw) = record.raw_fields.get(field) {
185 return Some(typed_value(kind.unwrap_or(FieldKind::Str), raw));
186 }
187 if field == "DESC" {
188 return Some(ScalarValue::Str(record.common.desc.clone()));
189 }
190 dbcommon_default(field).map(|(kind, default)| typed_value(kind, default))
191}
192
193pub fn payload_for(record: &RecordInstance, field_ref: &FieldRef) -> Option<NtPayload> {
199 let value = field_value(record, &field_ref.field)?;
200 if field_ref.long_string {
201 let ScalarValue::Str(s) = value else {
202 return None;
203 };
204 let bytes: Vec<i8> = s.into_bytes().into_iter().map(|b| b as i8).collect();
205 return Some(NtPayload::ScalarArray(NtScalarArray::from_value(
206 ScalarArrayValue::I8(bytes),
207 )));
208 }
209 let mut nt = NtScalar::from_value(value);
210 nt.display_description = record.common.desc.clone();
211 Some(NtPayload::Scalar(nt))
212}
213
214pub struct RecordFieldSource {
220 store: Arc<SimplePvStore>,
221 open_subs: Mutex<Vec<mpsc::Sender<NtPayload>>>,
225}
226
227impl RecordFieldSource {
228 pub fn new(store: Arc<SimplePvStore>) -> Self {
229 Self {
230 store,
231 open_subs: Mutex::new(Vec::new()),
232 }
233 }
234
235 async fn resolve(&self, name: &str) -> Option<NtPayload> {
236 let field_ref = parse_field_ref(name)?;
237 let record = self.store.get_record(&field_ref.base).await?;
238 payload_for(&record, &field_ref)
239 }
240}
241
242impl Source for RecordFieldSource {
243 fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>> {
244 let name = name.to_string();
245 Box::pin(async move {
246 let payload = self.resolve(&name).await?;
247 Some(PvInfo {
248 descriptor: descriptor_for_payload(&payload),
249 writable: false,
250 })
251 })
252 }
253
254 fn get(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + '_>> {
255 let name = name.to_string();
256 Box::pin(async move { self.resolve(&name).await })
257 }
258
259 fn put(
260 &self,
261 name: &str,
262 _value: &DecodedValue,
263 ) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + '_>> {
264 let name = name.to_string();
265 Box::pin(async move { Err(format!("field PV '{}' is read-only", name)) })
266 }
267
268 fn subscribe(
269 &self,
270 name: &str,
271 ) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + '_>> {
272 let name = name.to_string();
273 Box::pin(async move {
274 let initial = self.resolve(&name).await?;
275 let (tx, rx) = mpsc::channel(4);
276 let _ = tx.try_send(initial);
277 self.open_subs.lock().await.push(tx);
278 Some(rx)
279 })
280 }
281
282 fn names(&self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + '_>> {
283 Box::pin(async move { Vec::new() })
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use super::*;
292
293 #[test]
294 fn parses_simple_field_ref() {
295 let r = parse_field_ref("SIM:AO.RTYP").unwrap();
296 assert_eq!(
297 (r.base.as_str(), r.field.as_str(), r.long_string),
298 ("SIM:AO", "RTYP", false)
299 );
300 }
301
302 #[test]
303 fn parses_long_string_suffix() {
304 let r = parse_field_ref("SIM:AO.DESC$").unwrap();
305 assert_eq!((r.field.as_str(), r.long_string), ("DESC", true));
306 }
307
308 #[test]
309 fn rejects_non_field_names() {
310 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()); }
316
317 #[test]
318 fn dbcommon_defaults_cover_key_fields() {
319 assert!(matches!(
320 dbcommon_default("SCAN"),
321 Some((FieldKind::Str, "Passive"))
322 ));
323 assert!(matches!(
324 dbcommon_default("PINI"),
325 Some((FieldKind::Str, "NO"))
326 ));
327 assert!(matches!(
328 dbcommon_default("PHAS"),
329 Some((FieldKind::Int, "0"))
330 ));
331 assert!(matches!(
332 dbcommon_default("MDEL"),
333 Some((FieldKind::Double, "0"))
334 ));
335 assert!(dbcommon_default("NOTAFIELD").is_none());
336 }
337
338 #[test]
339 fn record_type_names_match_db_names() {
340 assert_eq!(record_type_name(&RecordType::Ao), "ao");
341 assert_eq!(record_type_name(&RecordType::StringIn), "stringin");
342 assert_eq!(record_type_name(&RecordType::LongIn), "longin");
343 }
344
345 fn test_record() -> RecordInstance {
346 let recs = crate::db::parse_db(
347 r#"
348record(ao, "SIM:AO") {
349 field(VAL, "2.34")
350 field(DESC, "A test output")
351 field(EGU, "V")
352 field(MDEL, "0.5")
353}"#,
354 )
355 .expect("parse");
356 recs.get("SIM:AO").expect("record present").clone()
357 }
358
359 #[test]
360 fn computed_fields_resolve() {
361 let r = test_record();
362 assert_eq!(field_value(&r, "RTYP"), Some(ScalarValue::Str("ao".into())));
363 assert_eq!(
364 field_value(&r, "NAME"),
365 Some(ScalarValue::Str("SIM:AO".into()))
366 );
367 assert_eq!(
368 field_value(&r, "DTYP"),
369 Some(ScalarValue::Str("Soft Channel".into()))
370 );
371 assert_eq!(field_value(&r, "VAL"), Some(ScalarValue::F64(2.34)));
372 }
373
374 #[test]
375 fn raw_db_fields_take_precedence_over_defaults() {
376 let r = test_record();
377 assert_eq!(
378 field_value(&r, "DESC"),
379 Some(ScalarValue::Str("A test output".into()))
380 );
381 assert_eq!(field_value(&r, "EGU"), Some(ScalarValue::Str("V".into())));
382 assert_eq!(field_value(&r, "MDEL"), Some(ScalarValue::F64(0.5)));
383 }
384
385 #[test]
386 fn dbcommon_defaults_fill_absent_fields() {
387 let r = test_record();
388 assert_eq!(
389 field_value(&r, "SCAN"),
390 Some(ScalarValue::Str("Passive".into()))
391 );
392 assert_eq!(field_value(&r, "PHAS"), Some(ScalarValue::I32(0)));
393 assert_eq!(field_value(&r, "ADEL"), Some(ScalarValue::F64(0.0)));
394 assert_eq!(field_value(&r, "NOTAFIELD"), None);
395 }
396
397 fn test_source() -> RecordFieldSource {
398 let recs = crate::db::parse_db(
399 r#"
400record(ao, "SIM:AO") {
401 field(VAL, "2.34")
402 field(DESC, "A test output")
403 field(MDEL, "0.5")
404}"#,
405 )
406 .expect("parse");
407 let store = SimplePvStore::new(recs, std::collections::HashMap::new(), Vec::new(), false);
408 RecordFieldSource::new(Arc::new(store))
409 }
410
411 #[tokio::test]
412 async fn claims_field_pvs_read_only() {
413 let src = test_source();
414 let info = src.claim("SIM:AO.RTYP").await.expect("claimed");
415 assert!(!info.writable);
416 match src.get("SIM:AO.RTYP").await.expect("payload") {
417 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::Str("ao".into())),
418 other => panic!("expected scalar, got {other:?}"),
419 }
420 }
421
422 #[tokio::test]
423 async fn does_not_claim_non_field_names() {
424 let src = test_source();
425 assert!(src.claim("SIM:AO").await.is_none()); assert!(src.claim("SIM:AO.NOTAFIELD").await.is_none());
427 assert!(src.claim("SIM:MISSING.RTYP").await.is_none());
428 assert!(src.claim("SIM:AO.MDEL$").await.is_none()); }
430
431 #[tokio::test]
432 async fn put_is_rejected() {
433 let src = test_source();
434 let err = src
435 .put("SIM:AO.DESC", &DecodedValue::Int32(1))
436 .await
437 .expect_err("put must fail");
438 assert!(err.contains("read-only"), "unexpected error: {err}");
439 }
440
441 #[tokio::test]
442 async fn long_string_serves_utf8_bytes() {
443 let src = test_source();
444 match src.get("SIM:AO.DESC$").await.expect("payload") {
445 NtPayload::ScalarArray(arr) => {
446 let ScalarArrayValue::I8(bytes) = arr.value else {
447 panic!("expected Int8 array, got {:?}", arr.value);
448 };
449 let expected: Vec<i8> = "A test output".bytes().map(|b| b as i8).collect();
450 assert_eq!(bytes, expected);
451 }
452 other => panic!("expected scalar array, got {other:?}"),
453 }
454 }
455
456 #[tokio::test]
457 async fn subscribe_delivers_initial_snapshot() {
458 let src = test_source();
459 let mut rx = src.subscribe("SIM:AO.RTYP").await.expect("subscribed");
460 match rx.recv().await.expect("initial value") {
461 NtPayload::Scalar(nt) => assert_eq!(nt.value, ScalarValue::Str("ao".into())),
462 other => panic!("expected scalar, got {other:?}"),
463 }
464 assert!(rx.try_recv().is_err());
466 }
467}