1use async_trait::async_trait;
4
5use crate::{
6 error::DriverResult,
7 types::{
8 BrowsePage, BrowsePageRequest, DriverCapabilities, SearchEvent, SearchIndexControlAction,
9 SearchIndexRequest, SearchIndexResponse, SearchIndexStatus, SearchRequest, TagId, TagValue,
10 TagWrite, WriteOutcome,
11 },
12};
13
14#[async_trait]
32pub trait Driver: Send + Sync {
33 async fn read(&self, tags: &[TagId]) -> DriverResult<Vec<TagValue>>;
42
43 async fn write(&self, tag: &TagId, value: TagWrite) -> DriverResult<WriteOutcome>;
50
51 async fn capabilities(&self) -> DriverResult<DriverCapabilities> {
53 Err(crate::error::DriverError::Unsupported {
54 operation: "capabilities",
55 })
56 }
57
58 async fn browse(&self, request: BrowsePageRequest) -> DriverResult<BrowsePage>;
62
63 async fn close_browse_session(&self, _session_id: &str) -> DriverResult<()> {
65 Err(crate::error::DriverError::Unsupported {
66 operation: "browse-session close",
67 })
68 }
69
70 async fn search(&self, _request: SearchRequest) -> DriverResult<Vec<SearchEvent>> {
73 Err(crate::error::DriverError::Unsupported {
74 operation: "search",
75 })
76 }
77
78 async fn search_index_status(&self) -> DriverResult<SearchIndexStatus> {
80 Err(crate::error::DriverError::Unsupported {
81 operation: "indexed-search status",
82 })
83 }
84
85 async fn refresh_search_index(&self, _force: bool) -> DriverResult<SearchIndexStatus> {
87 Err(crate::error::DriverError::Unsupported {
88 operation: "indexed-search refresh",
89 })
90 }
91
92 async fn control_search_index(
94 &self,
95 _action: SearchIndexControlAction,
96 ) -> DriverResult<SearchIndexStatus> {
97 Err(crate::error::DriverError::Unsupported {
98 operation: "indexed-search control",
99 })
100 }
101
102 async fn set_search_index_auto_refresh(
104 &self,
105 _enabled: bool,
106 ) -> DriverResult<SearchIndexStatus> {
107 Err(crate::error::DriverError::Unsupported {
108 operation: "indexed-search auto-refresh",
109 })
110 }
111
112 async fn delete_search_index(&self) -> DriverResult<SearchIndexStatus> {
114 Err(crate::error::DriverError::Unsupported {
115 operation: "indexed-search delete",
116 })
117 }
118
119 async fn search_index(
121 &self,
122 _request: SearchIndexRequest,
123 ) -> DriverResult<SearchIndexResponse> {
124 Err(crate::error::DriverError::Unsupported {
125 operation: "indexed search",
126 })
127 }
128}
129
130#[cfg(test)]
131mod tests {
132 use super::*;
133 use crate::{error::DriverError, types::Quality};
134 use chrono::{TimeZone, Utc};
135 use std::sync::Mutex;
136
137 struct BareDriver;
138
139 #[async_trait]
140 impl Driver for BareDriver {
141 async fn read(&self, _tags: &[TagId]) -> DriverResult<Vec<TagValue>> {
142 Ok(Vec::new())
143 }
144
145 async fn write(&self, _tag: &TagId, _value: TagWrite) -> DriverResult<WriteOutcome> {
146 Ok(WriteOutcome::success())
147 }
148
149 async fn browse(&self, _request: BrowsePageRequest) -> DriverResult<BrowsePage> {
150 Ok(BrowsePage {
151 session_id: "s".into(),
152 nodes: Vec::new(),
153 next_page_token: None,
154 complete: true,
155 organization: crate::types::NamespaceOrganization::Unspecified,
156 source: crate::types::BrowseSource::Unspecified,
157 warning: None,
158 })
159 }
160 }
161
162 struct MockDriver {
166 values: std::collections::HashMap<TagId, (String, Quality)>,
167 writes: Mutex<Vec<(TagId, TagWrite)>>,
168 }
169
170 #[async_trait]
171 impl Driver for MockDriver {
172 async fn read(&self, tags: &[TagId]) -> DriverResult<Vec<TagValue>> {
173 let timestamp = Some(Utc.with_ymd_and_hms(2024, 1, 1, 0, 0, 0).unwrap());
174 tags.iter()
175 .map(|tag| {
176 let (value, quality) = self.values.get(tag).cloned().ok_or_else(|| {
177 DriverError::InvalidTagValue {
178 tag: tag.clone(),
179 message: "unknown tag".to_string(),
180 }
181 })?;
182 Ok(TagValue {
183 tag: tag.clone(),
184 value,
185 quality,
186 timestamp,
187 })
188 })
189 .collect()
190 }
191
192 async fn write(&self, tag: &TagId, value: TagWrite) -> DriverResult<WriteOutcome> {
193 self.writes.lock().unwrap().push((tag.clone(), value));
194 Ok(WriteOutcome::success())
195 }
196
197 async fn capabilities(&self) -> DriverResult<DriverCapabilities> {
198 Ok(DriverCapabilities {
199 application_version: "test".into(),
200 protocol_version: "test".into(),
201 max_page_size: 200,
202 supports_browse_sessions: true,
203 supports_search: true,
204 organization: crate::types::NamespaceOrganization::Hierarchical,
205 source: crate::types::BrowseSource::Derived,
206 supports_indexed_search: false,
207 indexed_search_protocol_version: String::new(),
208 max_indexed_search_results: 0,
209 search_index_state: crate::types::SearchIndexState::Unspecified,
210 })
211 }
212
213 async fn browse(&self, _request: BrowsePageRequest) -> DriverResult<BrowsePage> {
214 Err(DriverError::Unsupported {
215 operation: "browse",
216 })
217 }
218
219 async fn close_browse_session(&self, _session_id: &str) -> DriverResult<()> {
220 Err(DriverError::Unsupported {
221 operation: "browse-session close",
222 })
223 }
224
225 async fn search(&self, _request: SearchRequest) -> DriverResult<Vec<SearchEvent>> {
226 Err(DriverError::Unsupported {
227 operation: "search",
228 })
229 }
230 }
231
232 fn mock() -> MockDriver {
233 let mut values = std::collections::HashMap::new();
234 values.insert(
235 "Area1.LIC101.PV".to_string(),
236 ("42.5".to_string(), Quality::Good),
237 );
238 values.insert(
239 "Area1.LIC101.MODE".to_string(),
240 ("MAN".to_string(), Quality::Uncertain),
241 );
242 MockDriver {
243 values,
244 writes: Mutex::new(Vec::new()),
245 }
246 }
247
248 #[tokio::test]
249 async fn reads_multiple_tags_in_requested_order() {
250 let driver = mock();
251 let tags = vec![
252 "Area1.LIC101.MODE".to_string(),
253 "Area1.LIC101.PV".to_string(),
254 ];
255 let values = driver.read(&tags).await.unwrap();
256 assert_eq!(values.len(), 2);
257 assert_eq!(values[0].tag, "Area1.LIC101.MODE");
258 assert_eq!(values[0].value, "MAN");
259 assert_eq!(values[0].quality, Quality::Uncertain);
260 assert_eq!(values[1].tag, "Area1.LIC101.PV");
261 assert_eq!(values[1].value, "42.5");
262 assert_eq!(values[1].quality, Quality::Good);
263 }
264
265 #[tokio::test]
266 async fn default_trait_operations_report_unsupported() {
267 let driver = BareDriver;
268 assert!(driver.read(&[]).await.unwrap().is_empty());
269 assert!(
270 driver
271 .write(&"MV".to_string(), TagWrite::Float(1.0))
272 .await
273 .unwrap()
274 .success
275 );
276 assert_eq!(
277 driver
278 .browse(BrowsePageRequest::root(1))
279 .await
280 .unwrap()
281 .session_id,
282 "s"
283 );
284 assert!(matches!(
285 driver.capabilities().await,
286 Err(DriverError::Unsupported {
287 operation: "capabilities"
288 })
289 ));
290 assert!(matches!(
291 driver.close_browse_session("session").await,
292 Err(DriverError::Unsupported {
293 operation: "browse-session close"
294 })
295 ));
296 assert!(matches!(
297 driver
298 .search(SearchRequest {
299 query: "PV".into(),
300 match_mode: crate::types::SearchMatchMode::Contains,
301 session_id: None,
302 scope_node_key: None,
303 max_results: 10,
304 include_branches: false,
305 refresh: false,
306 })
307 .await,
308 Err(DriverError::Unsupported {
309 operation: "search"
310 })
311 ));
312 assert!(matches!(
313 driver.search_index_status().await,
314 Err(DriverError::Unsupported {
315 operation: "indexed-search status"
316 })
317 ));
318 assert!(matches!(
319 driver.refresh_search_index(false).await,
320 Err(DriverError::Unsupported {
321 operation: "indexed-search refresh"
322 })
323 ));
324 assert!(matches!(
325 driver
326 .control_search_index(crate::types::SearchIndexControlAction::Pause)
327 .await,
328 Err(DriverError::Unsupported {
329 operation: "indexed-search control"
330 })
331 ));
332 assert!(matches!(
333 driver.set_search_index_auto_refresh(false).await,
334 Err(DriverError::Unsupported {
335 operation: "indexed-search auto-refresh"
336 })
337 ));
338 assert!(matches!(
339 driver.delete_search_index().await,
340 Err(DriverError::Unsupported {
341 operation: "indexed-search delete"
342 })
343 ));
344 assert!(matches!(
345 driver
346 .search_index(crate::types::SearchIndexRequest::new(
347 "PV",
348 crate::types::SearchMatchMode::Exact,
349 1,
350 ))
351 .await,
352 Err(DriverError::Unsupported {
353 operation: "indexed search"
354 })
355 ));
356 }
357
358 #[tokio::test]
359 async fn mock_driver_capabilities_and_explicit_unsupported_operations_are_callable() {
360 let driver = mock();
361 let capabilities = <MockDriver as Driver>::capabilities(&driver).await.unwrap();
362 assert_eq!(capabilities.application_version, "test");
363 assert!(matches!(
364 <MockDriver as Driver>::browse(&driver, BrowsePageRequest::root(1)).await,
365 Err(DriverError::Unsupported {
366 operation: "browse"
367 })
368 ));
369 assert!(matches!(
370 <MockDriver as Driver>::close_browse_session(&driver, "session").await,
371 Err(DriverError::Unsupported {
372 operation: "browse-session close"
373 })
374 ));
375 assert!(matches!(
376 <MockDriver as Driver>::search(
377 &driver,
378 SearchRequest {
379 query: "PV".into(),
380 match_mode: crate::types::SearchMatchMode::Exact,
381 session_id: None,
382 scope_node_key: None,
383 max_results: 1,
384 include_branches: false,
385 refresh: false,
386 },
387 )
388 .await,
389 Err(DriverError::Unsupported {
390 operation: "search"
391 })
392 ));
393 }
394
395 #[tokio::test]
396 async fn reading_an_unknown_tag_is_invalid_tag_value_not_a_panic() {
397 let driver = mock();
398 let err = driver
399 .read(&["Nonexistent.Tag".to_string()])
400 .await
401 .unwrap_err();
402 assert!(matches!(err, DriverError::InvalidTagValue { .. }));
403 }
404
405 #[tokio::test]
406 async fn write_records_the_call_and_reports_success() {
407 let driver = mock();
408 let outcome = driver
409 .write(&"Area1.LIC101.MV".to_string(), TagWrite::Float(55.0))
410 .await
411 .unwrap();
412 assert!(outcome.success);
413 assert_eq!(
414 driver.writes.lock().unwrap().as_slice(),
415 &[("Area1.LIC101.MV".to_string(), TagWrite::Float(55.0))]
416 );
417 }
418
419 #[tokio::test]
420 async fn write_supports_raw_mode_values_for_mode_revert() {
421 let driver = mock();
422 driver
423 .write(
424 &"Area1.LIC101.MODE".to_string(),
425 TagWrite::Raw("AUT".into()),
426 )
427 .await
428 .unwrap();
429 assert_eq!(
430 driver.writes.lock().unwrap().as_slice(),
431 &[("Area1.LIC101.MODE".to_string(), TagWrite::Raw("AUT".into()))]
432 );
433 }
434
435 #[tokio::test]
436 async fn browse_returns_unsupported_when_driver_has_no_tag_tree() {
437 let err = mock()
438 .browse(BrowsePageRequest::root(20))
439 .await
440 .unwrap_err();
441 assert!(matches!(
442 err,
443 DriverError::Unsupported {
444 operation: "browse"
445 }
446 ));
447 }
448
449 #[tokio::test]
450 async fn trait_is_object_safe_and_usable_through_a_trait_object() {
451 let driver: Box<dyn Driver> = Box::new(mock());
454 let values = driver.read(&["Area1.LIC101.PV".to_string()]).await.unwrap();
455 assert_eq!(values[0].value, "42.5");
456 }
457}