Skip to main content

bhtune_driver/
readonly.rs

1//! A driver adapter that forwards reads but never forwards writes.
2
3use async_trait::async_trait;
4
5use crate::{
6    Driver, DriverCapabilities, DriverError, DriverResult, TagId, TagValue, TagWrite, WriteOutcome,
7};
8
9/// Restricts a driver to read-only use.
10///
11/// Reads and capability discovery are forwarded. Writes and every other operation retain the
12/// trait's unsupported behavior, so a caller cannot accidentally use this wrapper to mutate a
13/// controller or a gateway-owned namespace index.
14pub struct ReadOnlyDriver {
15    inner: Box<dyn Driver>,
16}
17
18impl ReadOnlyDriver {
19    /// Wraps a driver for a read-only operation.
20    pub fn new(inner: Box<dyn Driver>) -> Self {
21        Self { inner }
22    }
23}
24
25#[async_trait]
26impl Driver for ReadOnlyDriver {
27    async fn read(&self, tags: &[TagId]) -> DriverResult<Vec<TagValue>> {
28        self.inner.read(tags).await
29    }
30
31    async fn write(&self, _tag: &TagId, _value: TagWrite) -> DriverResult<WriteOutcome> {
32        Err(DriverError::Unsupported { operation: "write" })
33    }
34
35    async fn capabilities(&self) -> DriverResult<DriverCapabilities> {
36        self.inner.capabilities().await
37    }
38
39    async fn browse(&self, _request: crate::BrowsePageRequest) -> DriverResult<crate::BrowsePage> {
40        Err(DriverError::Unsupported {
41            operation: "browse",
42        })
43    }
44}
45
46#[cfg(test)]
47mod tests {
48    use std::sync::{Arc, Mutex};
49
50    use super::*;
51    use crate::{
52        BrowseNodeKind, BrowsePage, BrowsePageRequest, BrowseSource, DriverCapabilities,
53        NamespaceOrganization, Quality,
54    };
55
56    struct RecordingDriver {
57        writes: Arc<Mutex<Vec<(TagId, TagWrite)>>>,
58    }
59
60    #[async_trait]
61    impl Driver for RecordingDriver {
62        async fn read(&self, tags: &[TagId]) -> DriverResult<Vec<TagValue>> {
63            Ok(tags
64                .iter()
65                .map(|tag| TagValue {
66                    tag: tag.clone(),
67                    value: "1".to_string(),
68                    quality: Quality::Good,
69                    timestamp: None,
70                })
71                .collect())
72        }
73
74        async fn write(&self, tag: &TagId, value: TagWrite) -> DriverResult<WriteOutcome> {
75            self.writes.lock().unwrap().push((tag.clone(), value));
76            Ok(WriteOutcome::success())
77        }
78
79        async fn capabilities(&self) -> DriverResult<DriverCapabilities> {
80            Ok(DriverCapabilities {
81                application_version: "test".to_string(),
82                protocol_version: "1".to_string(),
83                max_page_size: 1,
84                supports_browse_sessions: false,
85                supports_search: false,
86                organization: NamespaceOrganization::Unspecified,
87                source: BrowseSource::Unspecified,
88                supports_indexed_search: false,
89                indexed_search_protocol_version: String::new(),
90                max_indexed_search_results: 0,
91                search_index_state: crate::SearchIndexState::Unspecified,
92            })
93        }
94
95        async fn browse(&self, _request: BrowsePageRequest) -> DriverResult<BrowsePage> {
96            Ok(BrowsePage {
97                session_id: "test".to_string(),
98                nodes: vec![crate::BrowseNode {
99                    node_key: "node".to_string(),
100                    display_name: "Node".to_string(),
101                    kind: BrowseNodeKind::Item,
102                    item_id: Some("tag".to_string()),
103                }],
104                next_page_token: None,
105                complete: true,
106                organization: NamespaceOrganization::Unspecified,
107                source: BrowseSource::Unspecified,
108                warning: None,
109            })
110        }
111    }
112
113    #[tokio::test]
114    async fn attempted_write_does_not_reach_the_underlying_driver() {
115        let writes = Arc::new(Mutex::new(Vec::new()));
116        let driver = ReadOnlyDriver::new(Box::new(RecordingDriver {
117            writes: Arc::clone(&writes),
118        }));
119
120        assert!(matches!(
121            driver
122                .write(&"Unit1.LIC101.MV".to_string(), TagWrite::Float(50.0))
123                .await,
124            Err(DriverError::Unsupported { operation: "write" })
125        ));
126        assert!(writes.lock().unwrap().is_empty());
127    }
128
129    #[tokio::test]
130    async fn recording_driver_write_records_an_underlying_write() {
131        let writes = Arc::new(Mutex::new(Vec::new()));
132        let driver = RecordingDriver {
133            writes: Arc::clone(&writes),
134        };
135
136        driver
137            .write(&"Unit1.LIC101.MV".to_string(), TagWrite::Float(50.0))
138            .await
139            .unwrap();
140        assert_eq!(writes.lock().unwrap().len(), 1);
141    }
142
143    #[tokio::test]
144    async fn recording_driver_browse_returns_its_fixture_page() {
145        let driver = RecordingDriver {
146            writes: Arc::new(Mutex::new(Vec::new())),
147        };
148
149        let page = driver.browse(BrowsePageRequest::root(1)).await.unwrap();
150        assert_eq!(page.session_id, "test");
151        assert_eq!(page.nodes[0].item_id.as_deref(), Some("tag"));
152    }
153
154    #[tokio::test]
155    async fn read_only_driver_forwards_reads_and_capabilities_but_not_browse() {
156        let driver = ReadOnlyDriver::new(Box::new(RecordingDriver {
157            writes: Arc::new(Mutex::new(Vec::new())),
158        }));
159
160        let values = driver.read(&["Unit1.LIC101.PV".to_string()]).await.unwrap();
161        assert_eq!(values[0].value, "1");
162        assert_eq!(
163            driver.capabilities().await.unwrap().application_version,
164            "test"
165        );
166        assert!(matches!(
167            driver.browse(BrowsePageRequest::root(1)).await,
168            Err(DriverError::Unsupported {
169                operation: "browse"
170            })
171        ));
172    }
173}