Skip to main content

rs_teststand/property/
property_object_file.rs

1//! A file that stores property objects.
2
3use rs_teststand_sys::{Dispatch, Value};
4
5use crate::Error;
6use crate::dispids::property_object_file;
7use crate::types::TypeUsageList;
8
9/// A file holding property objects (`PropertyObjectFile`).
10///
11/// The file view of something that also has a richer identity, a sequence file
12/// reached through `as_property_object_file`, or a workspace's options file.
13/// This is where a file's registered types live.
14#[derive(Debug)]
15pub struct PropertyObjectFile {
16    dispatch: Box<dyn Dispatch>,
17}
18
19impl PropertyObjectFile {
20    /// Wraps a dispatch handle returned by the engine.
21    pub(crate) fn new(dispatch: Box<dyn Dispatch>) -> Self {
22        Self { dispatch }
23    }
24
25    /// Writes the file to disk if it has changed
26    /// (`PropertyObjectFile.SaveFileIfModified`).
27    ///
28    /// Does nothing when the file is unmodified. The path written is whatever
29    /// [`path`](Self::path) reports.
30    ///
31    /// **Pass `prompt = false` from a host with no operator.** With `true` the
32    /// engine puts a dialog on screen offering to save, and a headless caller
33    /// would block on a question nobody can answer. The returned `false` means
34    /// only that someone declined at that dialog, so under `prompt = false` a
35    /// `false` should not happen.
36    ///
37    /// # Errors
38    /// [`Error`] if the COM call fails or returns an unexpected type.
39    pub fn save_file_if_modified(&self, prompt: bool) -> Result<bool, Error> {
40        Ok(self
41            .dispatch
42            .call(
43                property_object_file::SAVE_FILE_IF_MODIFIED,
44                &[Value::Bool(prompt)],
45            )?
46            .as_bool()?)
47    }
48
49    /// The types registered in this file (`TypeUsageList`).
50    ///
51    /// # Errors
52    /// [`Error`] if the COM call fails or returns an unexpected type.
53    pub fn type_usage_list(&self) -> Result<TypeUsageList, Error> {
54        Ok(TypeUsageList::new(
55            self.dispatch
56                .get(property_object_file::TYPE_USAGE_LIST)?
57                .into_object()?,
58        ))
59    }
60
61    /// Marks the file as modified (`IncChangeCount`).
62    ///
63    /// Saving does nothing when the file does not believe it has changed, so a
64    /// change made through the API needs this before the save will write.
65    ///
66    /// # Errors
67    /// [`Error`] if the COM call fails.
68    pub fn inc_change_count(&self) -> Result<(), Error> {
69        self.dispatch
70            .call(property_object_file::INC_CHANGE_COUNT, &[])?;
71        Ok(())
72    }
73
74    /// Number of changes recorded since this file entered memory
75    /// (`PropertyObjectFile.ChangeCount`).
76    ///
77    /// # Errors
78    /// [`Error`] if the COM call fails or returns an unexpected type.
79    pub fn change_count(&self) -> Result<i32, Error> {
80        Ok(self
81            .dispatch
82            .get(property_object_file::CHANGE_COUNT)?
83            .as_i32()?)
84    }
85
86    /// Replaces the recorded change count (`PropertyObjectFile.ChangeCount`).
87    ///
88    /// # Errors
89    /// [`Error`] if the COM call fails.
90    pub fn set_change_count(&self, count: i32) -> Result<(), Error> {
91        self.dispatch
92            .put(property_object_file::CHANGE_COUNT, Value::I32(count))?;
93        Ok(())
94    }
95
96    /// Whether the in-memory file has changes not written to disk
97    /// (`PropertyObjectFile.IsModified`).
98    ///
99    /// # Errors
100    /// [`Error`] if the COM call fails or returns an unexpected type.
101    pub fn is_modified(&self) -> Result<bool, Error> {
102        Ok(self
103            .dispatch
104            .get(property_object_file::IS_MODIFIED)?
105            .as_bool()?)
106    }
107
108    /// The root of the file's property tree (`Data`).
109    ///
110    /// Everything a file stores hangs off here, which is how a file with no
111    /// richer identity, the templates file, for one, is read at all.
112    ///
113    /// # Errors
114    /// [`Error`] if the COM call fails or returns an unexpected type.
115    pub fn data(&self) -> Result<crate::property::PropertyObject, Error> {
116        Ok(crate::property::PropertyObject::new(
117            self.dispatch
118                .get(property_object_file::DATA)?
119                .into_object()?,
120        ))
121    }
122
123    /// The file's path (`Path`).
124    ///
125    /// # Errors
126    /// [`Error`] if the COM call fails or returns an unexpected type.
127    pub fn path(&self) -> Result<String, Error> {
128        Ok(self
129            .dispatch
130            .get(property_object_file::PATH)?
131            .into_string()?)
132    }
133
134    /// Sets the file's path (`Path`).
135    ///
136    /// # Errors
137    /// [`Error`] if the COM call fails.
138    pub fn set_path(&self, path: &str) -> Result<(), Error> {
139        self.dispatch
140            .put(property_object_file::PATH, Value::Str(path.to_owned()))?;
141        Ok(())
142    }
143
144    /// Whether the disk copy differs from this in-memory file
145    /// (`PropertyObjectFile.IsDiskFileModified`).
146    ///
147    /// `1` means disk is newer, `-1` means memory is newer, and `0` means the
148    /// two copies match.
149    ///
150    /// # Errors
151    /// [`Error`] if the COM call fails or returns an unexpected type.
152    pub fn is_disk_file_modified(&self) -> Result<i32, Error> {
153        Ok(self
154            .dispatch
155            .get(property_object_file::IS_DISK_FILE_MODIFIED)?
156            .as_i32()?)
157    }
158
159    /// Whether the disk path is read-only (`PropertyObjectFile.IsDiskFileReadOnly`).
160    ///
161    /// # Errors
162    /// [`Error`] if the COM call fails or returns an unexpected type.
163    pub fn is_disk_file_read_only(&self) -> Result<bool, Error> {
164        Ok(self
165            .dispatch
166            .get(property_object_file::IS_DISK_FILE_READ_ONLY)?
167            .as_bool()?)
168    }
169
170    /// Version string associated with this file (`PropertyObjectFile.Version`).
171    ///
172    /// # Errors
173    /// [`Error`] if the COM call fails or returns an unexpected type.
174    pub fn version(&self) -> Result<String, Error> {
175        Ok(self
176            .dispatch
177            .get(property_object_file::VERSION)?
178            .into_string()?)
179    }
180
181    /// Sets the file's version string (`PropertyObjectFile.Version`).
182    ///
183    /// # Errors
184    /// [`Error`] if the COM call fails.
185    pub fn set_version(&self, version: &str) -> Result<(), Error> {
186        self.dispatch.put(
187            property_object_file::VERSION,
188            Value::Str(version.to_owned()),
189        )?;
190        Ok(())
191    }
192}
193
194#[cfg(test)]
195mod tests {
196    use super::PropertyObjectFile;
197    use crate::Error;
198    use crate::dispids::property_object_file;
199    use rs_teststand_sys::{ComError, Dispatch, Value};
200    use std::collections::HashMap;
201
202    #[derive(Debug)]
203    struct FakeDispatch {
204        reads: HashMap<i32, Value>,
205    }
206
207    impl Dispatch for FakeDispatch {
208        fn get(&self, dispid: i32) -> Result<Value, ComError> {
209            match self.reads.get(&dispid) {
210                Some(Value::I32(value)) => Ok(Value::I32(*value)),
211                Some(Value::Bool(value)) => Ok(Value::Bool(*value)),
212                Some(Value::Str(value)) => Ok(Value::Str(value.clone())),
213                _ => Err(ComError::hresult(0, "fake: unscripted property")),
214            }
215        }
216
217        fn put(&self, _dispid: i32, _value: Value) -> Result<(), ComError> {
218            Ok(())
219        }
220
221        fn call(&self, _dispid: i32, _args: &[Value]) -> Result<Value, ComError> {
222            Err(ComError::hresult(0, "fake: unscripted method"))
223        }
224    }
225
226    fn file(reads: Vec<(i32, Value)>) -> PropertyObjectFile {
227        PropertyObjectFile::new(Box::new(FakeDispatch {
228            reads: reads.into_iter().collect(),
229        }))
230    }
231
232    #[test]
233    fn file_state_reads_use_their_typed_dispatch_properties() -> Result<(), Error> {
234        let subject = file(vec![
235            (property_object_file::CHANGE_COUNT, Value::I32(4)),
236            (property_object_file::IS_MODIFIED, Value::Bool(true)),
237            (property_object_file::IS_DISK_FILE_MODIFIED, Value::I32(-1)),
238            (
239                property_object_file::IS_DISK_FILE_READ_ONLY,
240                Value::Bool(false),
241            ),
242            (
243                property_object_file::VERSION,
244                Value::Str("1.2.3.4".to_owned()),
245            ),
246        ]);
247
248        assert_eq!(subject.change_count()?, 4);
249        assert!(subject.is_modified()?);
250        assert_eq!(subject.is_disk_file_modified()?, -1);
251        assert!(!subject.is_disk_file_read_only()?);
252        assert_eq!(subject.version()?, "1.2.3.4");
253        Ok(())
254    }
255
256    #[test]
257    fn file_state_writes_accept_typed_values() -> Result<(), Error> {
258        let subject = file(Vec::new());
259        subject.set_change_count(9)?;
260        subject.set_version("2.0.0.0")?;
261        Ok(())
262    }
263}