spvirit_server/pvstore.rs
1//! The [`Source`] trait — an object-safe abstraction over any PV data source,
2//! and [`SourceRegistry`] — a dynamic, priority-ordered collection of sources.
3//!
4//! Protocol handlers use `SourceRegistry` to resolve PV names across multiple
5//! registered sources, allowing different backends (in-memory records, hardware
6//! drivers, proxies, etc.) to coexist in a single PVA server. basically what pvxs does with its provider registry.
7
8use std::collections::HashSet;
9use std::future::Future;
10use std::pin::Pin;
11use std::sync::Arc;
12
13use spvirit_codec::spvd_decode::{DecodedValue, StructureDesc};
14use spvirit_types::NtPayload;
15use tokio::sync::{RwLock, mpsc};
16use tracing::debug;
17
18// ---------------------------------------------------------------------------
19// PvInfo — metadata returned by Source::claim
20// ---------------------------------------------------------------------------
21
22/// Metadata about a PV as reported by the source that owns it.
23#[derive(Debug, Clone)]
24pub struct PvInfo {
25 /// Structure descriptor for the PV.
26 pub descriptor: StructureDesc,
27 /// Whether the PV accepts PUT operations.
28 pub writable: bool,
29}
30
31// ---------------------------------------------------------------------------
32// Source — the object-safe provider trait
33// ---------------------------------------------------------------------------
34
35/// Object-safe trait for a PV data provider.
36///
37/// A source is responsible for a set of PV names. The server's
38/// [`SourceRegistry`] iterates sources in priority order to find the first
39/// that *claims* a given name.
40///
41/// # Implementing a custom source
42///
43/// ```rust,ignore
44/// use spvirit_server::pvstore::{Source, PvInfo};
45///
46/// struct MySource { /* ... */ }
47///
48/// impl Source for MySource {
49/// fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>> {
50/// Box::pin(async move { /* ... */ })
51/// }
52/// // ...other methods...
53/// }
54/// ```
55pub trait Source: Send + Sync {
56 /// Check whether this source owns `name` and, if so, return its metadata.
57 ///
58 /// Return `None` to let the registry try the next source.
59 fn claim(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<PvInfo>> + Send + '_>>;
60
61 /// Read the current value of a PV.
62 ///
63 /// Only called for PVs this source has previously claimed.
64 fn get(&self, name: &str) -> Pin<Box<dyn Future<Output = Option<NtPayload>> + Send + '_>>;
65
66 /// Apply a PUT value to a PV.
67 ///
68 /// Returns the list of `(pv_name, updated_payload)` pairs for all PVs
69 /// that changed as a result (e.g. forward-link propagation).
70 fn put(
71 &self,
72 name: &str,
73 value: &DecodedValue,
74 ) -> Pin<Box<dyn Future<Output = Result<Vec<(String, NtPayload)>, String>> + Send + '_>>;
75
76 /// Subscribe to value-change notifications on a PV.
77 ///
78 /// Returns `None` if the PV does not support subscription.
79 fn subscribe(
80 &self,
81 name: &str,
82 ) -> Pin<Box<dyn Future<Output = Option<mpsc::Receiver<NtPayload>>> + Send + '_>>;
83
84 /// Execute an RPC call on a channel.
85 ///
86 /// `name` is the channel name, `args` is the decoded request structure.
87 /// Returns the response payload on success.
88 ///
89 /// The default implementation returns an error — override it in sources
90 /// that provide RPC endpoints.
91 fn rpc(
92 &self,
93 _name: &str,
94 _args: &DecodedValue,
95 ) -> Pin<Box<dyn Future<Output = Result<NtPayload, String>> + Send + '_>> {
96 Box::pin(async { Err("RPC not supported".to_string()) })
97 }
98
99 /// List all PV names provided by this source.
100 fn names(&self) -> Pin<Box<dyn Future<Output = Vec<String>> + Send + '_>>;
101}
102
103// ---------------------------------------------------------------------------
104// SourceEntry — one registered source with its priority
105// ---------------------------------------------------------------------------
106
107struct SourceEntry {
108 /// Human-readable label for debugging / logging.
109 #[allow(dead_code)]
110 label: String,
111 /// Lower values are checked first.
112 order: i32,
113 /// The actual source implementation.
114 source: Arc<dyn Source>,
115}
116
117// ---------------------------------------------------------------------------
118// SourceRegistry — ordered collection of sources
119// ---------------------------------------------------------------------------
120
121/// A dynamic, priority-ordered registry of [`Source`] providers.
122///
123/// PV name resolution iterates sources from lowest `order` to highest and
124/// delegates to the first source that claims the name.
125pub struct SourceRegistry {
126 sources: RwLock<Vec<SourceEntry>>,
127}
128
129impl SourceRegistry {
130 /// Create an empty registry.
131 pub fn new() -> Self {
132 Self {
133 sources: RwLock::new(Vec::new()),
134 }
135 }
136
137 /// Register a new source at the given priority.
138 ///
139 /// Lower `order` values are queried first.
140 pub async fn add(&self, label: impl Into<String>, order: i32, source: Arc<dyn Source>) {
141 let label = label.into();
142 debug!(
143 "SourceRegistry: adding source '{}' at order {}",
144 label, order
145 );
146 let mut sources = self.sources.write().await;
147 sources.push(SourceEntry {
148 label,
149 order,
150 source,
151 });
152 sources.sort_by_key(|e| e.order);
153 }
154
155 /// Remove all sources with the given label.
156 pub async fn remove(&self, label: &str) {
157 debug!("SourceRegistry: removing source '{}'", label);
158 let mut sources = self.sources.write().await;
159 sources.retain(|e| e.label != label);
160 }
161
162 // ── Delegating operations ────────────────────────────────────────
163
164 /// Find the first source that claims `name` and return its metadata.
165 pub async fn claim(&self, name: &str) -> Option<PvInfo> {
166 let sources = self.sources.read().await;
167 for entry in sources.iter() {
168 if let Some(info) = entry.source.claim(name).await {
169 return Some(info);
170 }
171 }
172 None
173 }
174
175 /// Check whether any source claims the given PV name.
176 pub async fn has_pv(&self, name: &str) -> bool {
177 self.claim(name).await.is_some()
178 }
179
180 /// Get the value from the first source that claims the PV.
181 pub async fn get(&self, name: &str) -> Option<NtPayload> {
182 let sources = self.sources.read().await;
183 for entry in sources.iter() {
184 if entry.source.claim(name).await.is_some() {
185 return entry.source.get(name).await;
186 }
187 }
188 None
189 }
190
191 /// Get the structure descriptor from the first source that claims the PV.
192 pub async fn get_descriptor(&self, name: &str) -> Option<StructureDesc> {
193 self.claim(name).await.map(|info| info.descriptor)
194 }
195
196 /// Check if the PV is writable (via the first claiming source).
197 pub async fn is_writable(&self, name: &str) -> bool {
198 self.claim(name).await.is_some_and(|info| info.writable)
199 }
200
201 /// Delegate a PUT to the first source that claims the PV.
202 pub async fn put(
203 &self,
204 name: &str,
205 value: &DecodedValue,
206 ) -> Result<Vec<(String, NtPayload)>, String> {
207 let sources = self.sources.read().await;
208 for entry in sources.iter() {
209 if entry.source.claim(name).await.is_some() {
210 return entry.source.put(name, value).await;
211 }
212 }
213 Err(format!("PV '{}' not found", name))
214 }
215
216 /// Subscribe via the first source that claims the PV.
217 pub async fn subscribe(&self, name: &str) -> Option<mpsc::Receiver<NtPayload>> {
218 let sources = self.sources.read().await;
219 for entry in sources.iter() {
220 if entry.source.claim(name).await.is_some() {
221 return entry.source.subscribe(name).await;
222 }
223 }
224 None
225 }
226
227 /// Execute an RPC call via the first source that claims the channel.
228 pub async fn rpc(&self, name: &str, args: &DecodedValue) -> Result<NtPayload, String> {
229 let sources = self.sources.read().await;
230 for entry in sources.iter() {
231 if entry.source.claim(name).await.is_some() {
232 return entry.source.rpc(name, args).await;
233 }
234 }
235 Err(format!("RPC channel '{}' not found", name))
236 }
237
238 /// Collect all PV names from every registered source.
239 pub async fn names(&self) -> Vec<String> {
240 let sources = self.sources.read().await;
241 let mut seen = HashSet::new();
242 let mut all = Vec::new();
243 for entry in sources.iter() {
244 for name in entry.source.names().await {
245 if seen.insert(name.clone()) {
246 all.push(name);
247 }
248 }
249 }
250 all.sort();
251 all
252 }
253}
254
255impl Default for SourceRegistry {
256 fn default() -> Self {
257 Self::new()
258 }
259}