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}