Skip to main content

spvirit_server/
field_provider.rs

1//! The field-access seam shared by the two stores.
2//!
3//! EPICS Base separates resolution from access: `dbNameToAddr` looks a field
4//! up in the record type's field-description table and yields a `DBADDR`
5//! carrying type and size, and `dbGetField` then reads through it.
6//! [`RecordFieldProvider`] keeps that split — `field_descriptor` answers
7//! "does this field exist and what type is it" without reading the value, so
8//! `Source::claim` no longer has to perform a full read to answer a channel
9//! search.
10//!
11//! The seam is deliberately **value-level**, not record-level. The two record
12//! models are not variations on a theme: `SimplePvStore`'s `RecordInstance`
13//! resolves through a raw string map of whatever the `.db` literally said,
14//! while `spvirit-ioc`'s `Record` is fully typed and has no raw-field map at
15//! all. A record-level seam would force `spvirit-ioc` to fabricate a
16//! `RecordInstance` on every field read, and is lossy both ways.
17
18use std::future::Future;
19use std::pin::Pin;
20
21use spvirit_codec::spvd_decode::StructureDesc;
22use spvirit_types::{NtPayload, NtScalar, NtScalarArray, ScalarArrayValue, ScalarValue};
23
24use crate::pvstore::PvInfo;
25use crate::record_fields::{FieldKind, parse_field_ref, payload_for_value};
26use crate::simple_store::{SimplePvStore, descriptor_for_payload};
27
28/// What a field is, without reading it: the analogue of Base's `DBADDR`.
29///
30/// Only the scalar type is carried, because that is all a PVA structure
31/// descriptor depends on.
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct RecordFieldDesc {
34    pub kind: FieldKind,
35}
36
37/// A store that can resolve `<record>.<FIELD>` references.
38///
39/// The implementors are the two stores: `SimplePvStore` (below) and
40/// `spvirit_ioc::IocSource`. Both go through the free functions in this
41/// module, so `.FIELD` behaviour cannot drift between the tiers.
42pub trait RecordFieldProvider: Send + Sync {
43    /// Resolve a field's value. The analogue of `dbGetField`.
44    ///
45    /// Returns `None` when the record does not exist or does not carry the
46    /// field — an IOC would not serve either.
47    fn field_value(
48        &self,
49        base: &str,
50        field: &str,
51    ) -> Pin<Box<dyn Future<Output = Option<ScalarValue>> + Send + '_>>;
52
53    /// Resolve a field's existence and type without reading it. The analogue
54    /// of `dbNameToAddr`.
55    ///
56    /// Must agree with [`field_value`](Self::field_value): if this returns
57    /// `Some(d)`, a subsequent read must produce a value whose
58    /// [`field_kind_of`] is `d.kind`.
59    fn field_descriptor(
60        &self,
61        base: &str,
62        field: &str,
63    ) -> Pin<Box<dyn Future<Output = Option<RecordFieldDesc>> + Send + '_>>;
64}
65
66/// The [`FieldKind`] a scalar value serves as.
67pub fn field_kind_of(value: &ScalarValue) -> FieldKind {
68    match value {
69        ScalarValue::Str(_) => FieldKind::Str,
70        ScalarValue::F32(_) | ScalarValue::F64(_) => FieldKind::Double,
71        _ => FieldKind::Int,
72    }
73}
74
75/// The PVA structure descriptor for a field of `kind`, without a value.
76///
77/// Built by describing a zero-valued probe payload rather than by
78/// hand-rolling a second descriptor table — the descriptor a claim
79/// advertises is then, by construction, the one `descriptor_for_payload`
80/// derives from the payload a get actually serves. `None` for the `$` form
81/// on a non-string field, which QSRV does not serve either.
82pub fn descriptor_for_kind(kind: FieldKind, long_string: bool) -> Option<StructureDesc> {
83    if long_string {
84        if kind != FieldKind::Str {
85            return None;
86        }
87        return Some(descriptor_for_payload(&NtPayload::ScalarArray(
88            NtScalarArray::from_value(ScalarArrayValue::I8(Vec::new())),
89        )));
90    }
91    let probe = match kind {
92        FieldKind::Str => ScalarValue::Str(String::new()),
93        FieldKind::Int => ScalarValue::I32(0),
94        FieldKind::Double => ScalarValue::F64(0.0),
95    };
96    Some(descriptor_for_payload(&NtPayload::Scalar(
97        NtScalar::from_value(probe),
98    )))
99}
100
101/// Resolve `<base>.<FIELD>[$]` to a wire payload through `provider`.
102///
103/// The record's `DESC` is fetched as a second field read and carried as the
104/// payload's `display_description`, matching what the record-level
105/// `payload_for` has always done. A record with no `DESC` yields an empty
106/// description, not a failure.
107pub async fn resolve_field_payload(
108    provider: &dyn RecordFieldProvider,
109    name: &str,
110) -> Option<NtPayload> {
111    let field_ref = parse_field_ref(name)?;
112    let value = provider
113        .field_value(&field_ref.base, &field_ref.field)
114        .await?;
115    let desc = match provider.field_value(&field_ref.base, "DESC").await {
116        Some(ScalarValue::Str(s)) => s,
117        _ => String::new(),
118    };
119    payload_for_value(value, &desc, field_ref.long_string)
120}
121
122/// Resolve `<base>.<FIELD>[$]` to channel metadata through `provider`,
123/// without reading the value.
124///
125/// Field PVs are read-only in A2: field writes are sub-project B's, matching
126/// Base's separate `dbPutField` verb.
127pub async fn resolve_field_info(provider: &dyn RecordFieldProvider, name: &str) -> Option<PvInfo> {
128    let field_ref = parse_field_ref(name)?;
129    let desc = provider
130        .field_descriptor(&field_ref.base, &field_ref.field)
131        .await?;
132    Some(PvInfo {
133        descriptor: descriptor_for_kind(desc.kind, field_ref.long_string)?,
134        writable: false,
135    })
136}
137
138impl RecordFieldProvider for SimplePvStore {
139    fn field_value(
140        &self,
141        base: &str,
142        field: &str,
143    ) -> Pin<Box<dyn Future<Output = Option<ScalarValue>> + Send + '_>> {
144        let (base, field) = (base.to_string(), field.to_string());
145        Box::pin(async move {
146            let record = self.get_record(&base).await?;
147            crate::record_fields::field_value(&record, &field)
148        })
149    }
150
151    /// Tier 1's cheap path is the same read: `field_value` resolves through
152    /// an in-memory string map, so there is nothing cheaper to do. The split
153    /// pays off on the IOC path, where a value read takes a lock-set mutex
154    /// and a descriptor read does not.
155    ///
156    /// Deriving the kind from the value (rather than from `dbcommon_default`
157    /// alone) is deliberate: `typed_value` falls back to `Str` when a raw
158    /// `.db` string does not parse as its declared kind, so
159    /// `field(MDEL, "abc")` really does serve a string and the descriptor
160    /// must say so.
161    fn field_descriptor(
162        &self,
163        base: &str,
164        field: &str,
165    ) -> Pin<Box<dyn Future<Output = Option<RecordFieldDesc>> + Send + '_>> {
166        let (base, field) = (base.to_string(), field.to_string());
167        Box::pin(async move {
168            let record = self.get_record(&base).await?;
169            let value = crate::record_fields::field_value(&record, &field)?;
170            Some(RecordFieldDesc {
171                kind: field_kind_of(&value),
172            })
173        })
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use crate::record_fields::FieldKind;
181    use spvirit_types::{NtPayload, ScalarValue};
182    use std::collections::HashMap;
183    use std::future::Future;
184    use std::pin::Pin;
185
186    /// A provider with one record and three fields, and a counter proving
187    /// which method the caller actually used.
188    struct FakeProvider {
189        fields: HashMap<&'static str, ScalarValue>,
190        value_calls: std::sync::atomic::AtomicUsize,
191    }
192
193    impl FakeProvider {
194        fn new() -> Self {
195            let mut fields = HashMap::new();
196            fields.insert("RTYP", ScalarValue::Str("ao".into()));
197            fields.insert("DESC", ScalarValue::Str("A test output".into()));
198            fields.insert("VAL", ScalarValue::F64(2.34));
199            Self {
200                fields,
201                value_calls: std::sync::atomic::AtomicUsize::new(0),
202            }
203        }
204        fn value_calls(&self) -> usize {
205            self.value_calls.load(std::sync::atomic::Ordering::SeqCst)
206        }
207    }
208
209    impl RecordFieldProvider for FakeProvider {
210        fn field_value(
211            &self,
212            base: &str,
213            field: &str,
214        ) -> Pin<Box<dyn Future<Output = Option<ScalarValue>> + Send + '_>> {
215            let (base, field) = (base.to_string(), field.to_string());
216            Box::pin(async move {
217                self.value_calls
218                    .fetch_add(1, std::sync::atomic::Ordering::SeqCst);
219                if base != "SIM:AO" {
220                    return None;
221                }
222                self.fields.get(field.as_str()).cloned()
223            })
224        }
225
226        fn field_descriptor(
227            &self,
228            base: &str,
229            field: &str,
230        ) -> Pin<Box<dyn Future<Output = Option<RecordFieldDesc>> + Send + '_>> {
231            let (base, field) = (base.to_string(), field.to_string());
232            Box::pin(async move {
233                if base != "SIM:AO" {
234                    return None;
235                }
236                self.fields.get(field.as_str()).map(|v| RecordFieldDesc {
237                    kind: field_kind_of(v),
238                })
239            })
240        }
241    }
242
243    #[tokio::test]
244    async fn resolves_a_field_payload_with_the_records_description() {
245        let p = FakeProvider::new();
246        match resolve_field_payload(&p, "SIM:AO.RTYP")
247            .await
248            .expect("resolved")
249        {
250            NtPayload::Scalar(nt) => {
251                assert_eq!(nt.value, ScalarValue::Str("ao".into()));
252                assert_eq!(nt.display_description, "A test output");
253            }
254            other => panic!("expected scalar, got {other:?}"),
255        }
256    }
257
258    #[tokio::test]
259    async fn does_not_resolve_unknown_bases_fields_or_bare_names() {
260        let p = FakeProvider::new();
261        assert!(resolve_field_payload(&p, "SIM:AO").await.is_none());
262        assert!(resolve_field_payload(&p, "SIM:AO.NOTAFIELD").await.is_none());
263        assert!(resolve_field_payload(&p, "SIM:MISSING.RTYP").await.is_none());
264    }
265
266    #[tokio::test]
267    async fn resolve_field_info_never_reads_the_value() {
268        let p = FakeProvider::new();
269        let info = resolve_field_info(&p, "SIM:AO.VAL").await.expect("claimed");
270        assert!(!info.writable, "field PVs are read-only in A2");
271        assert_eq!(
272            p.value_calls(),
273            0,
274            "claim must answer from field_descriptor alone — this is the \
275             dbNameToAddr/dbGetField split the seam exists for"
276        );
277    }
278
279    #[tokio::test]
280    async fn the_descriptor_matches_the_payload_the_value_would_produce() {
281        let p = FakeProvider::new();
282        for name in ["SIM:AO.VAL", "SIM:AO.RTYP"] {
283            let info = resolve_field_info(&p, name).await.expect("claimed");
284            let payload = resolve_field_payload(&p, name).await.expect("resolved");
285            assert_eq!(
286                info.descriptor,
287                crate::simple_store::descriptor_for_payload(&payload),
288                "{name}: claim's descriptor must match what get actually serves"
289            );
290        }
291    }
292
293    #[tokio::test]
294    async fn long_string_claims_only_string_fields() {
295        let p = FakeProvider::new();
296        assert!(resolve_field_info(&p, "SIM:AO.DESC$").await.is_some());
297        assert!(resolve_field_info(&p, "SIM:AO.VAL$").await.is_none());
298    }
299
300    #[test]
301    fn field_kind_maps_scalar_variants() {
302        assert_eq!(field_kind_of(&ScalarValue::Str("x".into())), FieldKind::Str);
303        assert_eq!(field_kind_of(&ScalarValue::F64(1.0)), FieldKind::Double);
304        assert_eq!(field_kind_of(&ScalarValue::F32(1.0)), FieldKind::Double);
305        assert_eq!(field_kind_of(&ScalarValue::I32(1)), FieldKind::Int);
306        assert_eq!(field_kind_of(&ScalarValue::Bool(true)), FieldKind::Int);
307    }
308}