use std::future::Future;
use std::pin::Pin;
use spvirit_codec::spvd_decode::StructureDesc;
use spvirit_types::{NtPayload, NtScalar, NtScalarArray, ScalarArrayValue, ScalarValue};
use crate::pvstore::PvInfo;
use crate::record_fields::{FieldKind, parse_field_ref, payload_for_value};
use crate::simple_store::{SimplePvStore, descriptor_for_payload};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RecordFieldDesc {
pub kind: FieldKind,
}
pub trait RecordFieldProvider: Send + Sync {
fn field_value(
&self,
base: &str,
field: &str,
) -> Pin<Box<dyn Future<Output = Option<ScalarValue>> + Send + '_>>;
fn field_descriptor(
&self,
base: &str,
field: &str,
) -> Pin<Box<dyn Future<Output = Option<RecordFieldDesc>> + Send + '_>>;
}
pub fn field_kind_of(value: &ScalarValue) -> FieldKind {
match value {
ScalarValue::Str(_) => FieldKind::Str,
ScalarValue::F32(_) | ScalarValue::F64(_) => FieldKind::Double,
_ => FieldKind::Int,
}
}
pub fn descriptor_for_kind(kind: FieldKind, long_string: bool) -> Option<StructureDesc> {
if long_string {
if kind != FieldKind::Str {
return None;
}
return Some(descriptor_for_payload(&NtPayload::ScalarArray(
NtScalarArray::from_value(ScalarArrayValue::I8(Vec::new())),
)));
}
let probe = match kind {
FieldKind::Str => ScalarValue::Str(String::new()),
FieldKind::Int => ScalarValue::I32(0),
FieldKind::Double => ScalarValue::F64(0.0),
};
Some(descriptor_for_payload(&NtPayload::Scalar(
NtScalar::from_value(probe),
)))
}
pub async fn resolve_field_payload(
provider: &dyn RecordFieldProvider,
name: &str,
) -> Option<NtPayload> {
let field_ref = parse_field_ref(name)?;
let value = provider
.field_value(&field_ref.base, &field_ref.field)
.await?;
let desc = match provider.field_value(&field_ref.base, "DESC").await {
Some(ScalarValue::Str(s)) => s,
_ => String::new(),
};
payload_for_value(value, &desc, field_ref.long_string)
}
pub async fn resolve_field_info(provider: &dyn RecordFieldProvider, name: &str) -> Option<PvInfo> {
let field_ref = parse_field_ref(name)?;
let desc = provider
.field_descriptor(&field_ref.base, &field_ref.field)
.await?;
Some(PvInfo {
descriptor: descriptor_for_kind(desc.kind, field_ref.long_string)?,
writable: false,
})
}
impl RecordFieldProvider for SimplePvStore {
fn field_value(
&self,
base: &str,
field: &str,
) -> Pin<Box<dyn Future<Output = Option<ScalarValue>> + Send + '_>> {
let (base, field) = (base.to_string(), field.to_string());
Box::pin(async move {
let record = self.get_record(&base).await?;
crate::record_fields::field_value(&record, &field)
})
}
fn field_descriptor(
&self,
base: &str,
field: &str,
) -> Pin<Box<dyn Future<Output = Option<RecordFieldDesc>> + Send + '_>> {
let (base, field) = (base.to_string(), field.to_string());
Box::pin(async move {
let record = self.get_record(&base).await?;
let value = crate::record_fields::field_value(&record, &field)?;
Some(RecordFieldDesc {
kind: field_kind_of(&value),
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::record_fields::FieldKind;
use spvirit_types::{NtPayload, ScalarValue};
use std::collections::HashMap;
use std::future::Future;
use std::pin::Pin;
struct FakeProvider {
fields: HashMap<&'static str, ScalarValue>,
value_calls: std::sync::atomic::AtomicUsize,
}
impl FakeProvider {
fn new() -> Self {
let mut fields = HashMap::new();
fields.insert("RTYP", ScalarValue::Str("ao".into()));
fields.insert("DESC", ScalarValue::Str("A test output".into()));
fields.insert("VAL", ScalarValue::F64(2.34));
Self {
fields,
value_calls: std::sync::atomic::AtomicUsize::new(0),
}
}
fn value_calls(&self) -> usize {
self.value_calls.load(std::sync::atomic::Ordering::SeqCst)
}
}
impl RecordFieldProvider for FakeProvider {
fn field_value(
&self,
base: &str,
field: &str,
) -> Pin<Box<dyn Future<Output = Option<ScalarValue>> + Send + '_>> {
let (base, field) = (base.to_string(), field.to_string());
Box::pin(async move {
self.value_calls
.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
if base != "SIM:AO" {
return None;
}
self.fields.get(field.as_str()).cloned()
})
}
fn field_descriptor(
&self,
base: &str,
field: &str,
) -> Pin<Box<dyn Future<Output = Option<RecordFieldDesc>> + Send + '_>> {
let (base, field) = (base.to_string(), field.to_string());
Box::pin(async move {
if base != "SIM:AO" {
return None;
}
self.fields.get(field.as_str()).map(|v| RecordFieldDesc {
kind: field_kind_of(v),
})
})
}
}
#[tokio::test]
async fn resolves_a_field_payload_with_the_records_description() {
let p = FakeProvider::new();
match resolve_field_payload(&p, "SIM:AO.RTYP")
.await
.expect("resolved")
{
NtPayload::Scalar(nt) => {
assert_eq!(nt.value, ScalarValue::Str("ao".into()));
assert_eq!(nt.display_description, "A test output");
}
other => panic!("expected scalar, got {other:?}"),
}
}
#[tokio::test]
async fn does_not_resolve_unknown_bases_fields_or_bare_names() {
let p = FakeProvider::new();
assert!(resolve_field_payload(&p, "SIM:AO").await.is_none());
assert!(resolve_field_payload(&p, "SIM:AO.NOTAFIELD").await.is_none());
assert!(resolve_field_payload(&p, "SIM:MISSING.RTYP").await.is_none());
}
#[tokio::test]
async fn resolve_field_info_never_reads_the_value() {
let p = FakeProvider::new();
let info = resolve_field_info(&p, "SIM:AO.VAL").await.expect("claimed");
assert!(!info.writable, "field PVs are read-only in A2");
assert_eq!(
p.value_calls(),
0,
"claim must answer from field_descriptor alone — this is the \
dbNameToAddr/dbGetField split the seam exists for"
);
}
#[tokio::test]
async fn the_descriptor_matches_the_payload_the_value_would_produce() {
let p = FakeProvider::new();
for name in ["SIM:AO.VAL", "SIM:AO.RTYP"] {
let info = resolve_field_info(&p, name).await.expect("claimed");
let payload = resolve_field_payload(&p, name).await.expect("resolved");
assert_eq!(
info.descriptor,
crate::simple_store::descriptor_for_payload(&payload),
"{name}: claim's descriptor must match what get actually serves"
);
}
}
#[tokio::test]
async fn long_string_claims_only_string_fields() {
let p = FakeProvider::new();
assert!(resolve_field_info(&p, "SIM:AO.DESC$").await.is_some());
assert!(resolve_field_info(&p, "SIM:AO.VAL$").await.is_none());
}
#[test]
fn field_kind_maps_scalar_variants() {
assert_eq!(field_kind_of(&ScalarValue::Str("x".into())), FieldKind::Str);
assert_eq!(field_kind_of(&ScalarValue::F64(1.0)), FieldKind::Double);
assert_eq!(field_kind_of(&ScalarValue::F32(1.0)), FieldKind::Double);
assert_eq!(field_kind_of(&ScalarValue::I32(1)), FieldKind::Int);
assert_eq!(field_kind_of(&ScalarValue::Bool(true)), FieldKind::Int);
}
}