1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct RecordFieldDesc {
34 pub kind: FieldKind,
35}
36
37pub trait RecordFieldProvider: Send + Sync {
43 fn field_value(
48 &self,
49 base: &str,
50 field: &str,
51 ) -> Pin<Box<dyn Future<Output = Option<ScalarValue>> + Send + '_>>;
52
53 fn field_descriptor(
60 &self,
61 base: &str,
62 field: &str,
63 ) -> Pin<Box<dyn Future<Output = Option<RecordFieldDesc>> + Send + '_>>;
64}
65
66pub 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
75pub 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
101pub 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
122pub 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 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 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}