rs_teststand/property/property_object.rs
1//! TestStand `PropertyObject` (`IPropertyObject`) wrapper.
2
3use crate::Error;
4use crate::dispids::property_object;
5use crate::dispids::property_object_introspect as introspect;
6use crate::enums::PropValType;
7use rs_teststand_sys::{Dispatch, Value};
8
9/// Safe wrapper for TestStand™ `PropertyObject` (`IPropertyObject`).
10#[derive(Debug)]
11pub struct PropertyObject {
12 dispatch: Box<dyn Dispatch>,
13}
14
15impl PropertyObject {
16 /// Creates a new `PropertyObject` wrapper around a COM dispatch seam.
17 pub(crate) fn new(dispatch: Box<dyn Dispatch>) -> Self {
18 Self { dispatch }
19 }
20
21 /// Returns the underlying `Dispatch` reference for internal COM calls.
22 pub(crate) fn as_dispatch(&self) -> &dyn Dispatch {
23 &*self.dispatch
24 }
25
26 /// Checks if a property exists by lookup path (`PropertyObject.Exists`).
27 ///
28 /// # Errors
29 /// [`Error`] if the COM call fails or returns an unexpected type.
30 pub fn exists(&self, lookup_string: &str, options: i32) -> Result<bool, Error> {
31 Ok(self
32 .dispatch
33 .call(
34 property_object::EXISTS,
35 &[Value::Str(lookup_string.to_string()), Value::I32(options)],
36 )?
37 .as_bool()?)
38 }
39
40 /// Reads a string property by lookup path (`PropertyObject.GetValString`).
41 ///
42 /// # Errors
43 /// [`Error`] if the COM call fails or returns an unexpected type.
44 pub fn get_val_string(&self, lookup_string: &str, options: i32) -> Result<String, Error> {
45 Ok(self
46 .dispatch
47 .call(
48 property_object::GET_VAL_STRING,
49 &[Value::Str(lookup_string.to_string()), Value::I32(options)],
50 )?
51 .into_string()?)
52 }
53
54 /// Writes a string property by lookup path (`PropertyObject.SetValString`).
55 ///
56 /// # Errors
57 /// [`Error`] if the COM call fails.
58 pub fn set_val_string(
59 &self,
60 lookup_string: &str,
61 options: i32,
62 value: &str,
63 ) -> Result<(), Error> {
64 self.dispatch.call(
65 property_object::SET_VAL_STRING,
66 &[
67 Value::Str(lookup_string.to_string()),
68 Value::I32(options),
69 Value::Str(value.to_string()),
70 ],
71 )?;
72 Ok(())
73 }
74
75 /// Reads a numeric property by lookup path (`PropertyObject.GetValNumber`).
76 ///
77 /// # Errors
78 /// [`Error`] if the COM call fails or returns an unexpected type.
79 pub fn get_val_number(&self, lookup_string: &str, options: i32) -> Result<f64, Error> {
80 Ok(self
81 .dispatch
82 .call(
83 property_object::GET_VAL_NUMBER,
84 &[Value::Str(lookup_string.to_string()), Value::I32(options)],
85 )?
86 .as_f64()?)
87 }
88
89 /// Writes a numeric property by lookup path (`PropertyObject.SetValNumber`).
90 ///
91 /// # Errors
92 /// [`Error`] if the COM call fails.
93 pub fn set_val_number(
94 &self,
95 lookup_string: &str,
96 options: i32,
97 value: f64,
98 ) -> Result<(), Error> {
99 self.dispatch.call(
100 property_object::SET_VAL_NUMBER,
101 &[
102 Value::Str(lookup_string.to_string()),
103 Value::I32(options),
104 Value::F64(value),
105 ],
106 )?;
107 Ok(())
108 }
109
110 /// Reads a boolean property by lookup path (`PropertyObject.GetValBoolean`).
111 ///
112 /// # Errors
113 /// [`Error`] if the COM call fails or returns an unexpected type.
114 pub fn get_val_bool(&self, lookup_string: &str, options: i32) -> Result<bool, Error> {
115 Ok(self
116 .dispatch
117 .call(
118 property_object::GET_VAL_BOOLEAN,
119 &[Value::Str(lookup_string.to_string()), Value::I32(options)],
120 )?
121 .as_bool()?)
122 }
123
124 /// Writes a boolean property by lookup path (`PropertyObject.SetValBoolean`).
125 ///
126 /// # Errors
127 /// [`Error`] if the COM call fails.
128 pub fn set_val_bool(
129 &self,
130 lookup_string: &str,
131 options: i32,
132 value: bool,
133 ) -> Result<(), Error> {
134 self.dispatch.call(
135 property_object::SET_VAL_BOOLEAN,
136 &[
137 Value::Str(lookup_string.to_string()),
138 Value::I32(options),
139 Value::Bool(value),
140 ],
141 )?;
142 Ok(())
143 }
144
145 /// How many sub-properties sit directly under `lookup_string`
146 /// (`GetNumSubProperties`).
147 ///
148 /// Pass an empty string for this object itself.
149 ///
150 /// # Errors
151 /// [`Error`] if the COM call fails or returns an unexpected type.
152 pub fn get_num_sub_properties(&self, lookup_string: &str) -> Result<i32, Error> {
153 Ok(self
154 .dispatch
155 .call(
156 introspect::GET_NUM_SUB_PROPERTIES,
157 &[Value::Str(lookup_string.to_owned())],
158 )?
159 .as_i32()?)
160 }
161
162 /// The name of the `index`-th sub-property (`GetNthSubPropertyName`).
163 ///
164 /// # Errors
165 /// [`Error`] if the COM call fails or returns an unexpected type.
166 pub fn get_nth_sub_property_name(
167 &self,
168 lookup_string: &str,
169 index: i32,
170 options: i32,
171 ) -> Result<String, Error> {
172 Ok(self
173 .dispatch
174 .call(
175 introspect::GET_NTH_SUB_PROPERTY_NAME,
176 &[
177 Value::Str(lookup_string.to_owned()),
178 Value::I32(index),
179 Value::I32(options),
180 ],
181 )?
182 .into_string()?)
183 }
184
185 /// The `index`-th sub-property itself (`GetNthSubProperty`).
186 ///
187 /// # Errors
188 /// [`Error`] if the COM call fails or returns an unexpected type.
189 pub fn get_nth_sub_property(
190 &self,
191 lookup_string: &str,
192 index: i32,
193 options: i32,
194 ) -> Result<Self, Error> {
195 Ok(Self::new(
196 self.dispatch
197 .call(
198 introspect::GET_NTH_SUB_PROPERTY,
199 &[
200 Value::Str(lookup_string.to_owned()),
201 Value::I32(index),
202 Value::I32(options),
203 ],
204 )?
205 .into_object()?,
206 ))
207 }
208
209 /// Reads a signed 64-bit integer property (`GetValInteger64`).
210 ///
211 /// The engine stores a number as one of three things, a double, a signed
212 /// 64-bit integer, or an unsigned one, and the accessor must match. Use
213 /// this when [`property_type`](Self::property_type) reports
214 /// [`PropertyRepresentation::Int64`](crate::PropertyRepresentation::Int64);
215 /// [`get_val_number`](Self::get_val_number) fails on such a property rather
216 /// than converting.
217 ///
218 /// # Errors
219 /// [`Error`] if the COM call fails or the property is not stored as a
220 /// signed 64-bit integer.
221 pub fn get_val_integer64(&self, lookup_string: &str, options: i32) -> Result<i64, Error> {
222 Ok(self
223 .dispatch
224 .call(
225 introspect::GET_VAL_INTEGER64,
226 &[Value::Str(lookup_string.to_owned()), Value::I32(options)],
227 )?
228 .as_i64()?)
229 }
230
231 /// Writes a signed 64-bit integer property (`SetValInteger64`).
232 ///
233 /// # Errors
234 /// [`Error`] if the COM call fails.
235 pub fn set_val_integer64(
236 &self,
237 lookup_string: &str,
238 options: i32,
239 value: i64,
240 ) -> Result<(), Error> {
241 self.dispatch.call(
242 introspect::SET_VAL_INTEGER64,
243 &[
244 Value::Str(lookup_string.to_owned()),
245 Value::I32(options),
246 Value::I64(value),
247 ],
248 )?;
249 Ok(())
250 }
251
252 /// Reads an unsigned 64-bit integer property (`GetValUnsignedInteger64`).
253 ///
254 /// Use this when the representation is
255 /// [`UInt64`](crate::PropertyRepresentation::UInt64). The value crosses the
256 /// COM boundary as `VT_UI8` and is returned with its bits intact, so the
257 /// full unsigned range survives.
258 ///
259 /// # Errors
260 /// [`Error`] if the COM call fails or the property is not stored as an
261 /// unsigned 64-bit integer.
262 pub fn get_val_unsigned_integer64(
263 &self,
264 lookup_string: &str,
265 options: i32,
266 ) -> Result<u64, Error> {
267 let raw = self
268 .dispatch
269 .call(
270 introspect::GET_VAL_UNSIGNED_INTEGER64,
271 &[Value::Str(lookup_string.to_owned()), Value::I32(options)],
272 )?
273 .as_i64()?;
274 // The sys layer carries VT_UI8 as i64 to keep one integer variant;
275 // reinterpreting restores the unsigned reading of the same bits.
276 // `cast_unsigned` would be clearer but postdates this crate's MSRV.
277 #[allow(clippy::cast_sign_loss, reason = "bit-preserving reinterpretation")]
278 Ok(raw as u64)
279 }
280
281 /// Writes an unsigned 64-bit integer property (`SetValUnsignedInteger64`).
282 ///
283 /// # Errors
284 /// [`Error`] if the COM call fails.
285 pub fn set_val_unsigned_integer64(
286 &self,
287 lookup_string: &str,
288 options: i32,
289 value: u64,
290 ) -> Result<(), Error> {
291 self.dispatch.call(
292 introspect::SET_VAL_UNSIGNED_INTEGER64,
293 &[
294 Value::Str(lookup_string.to_owned()),
295 Value::I32(options),
296 Value::U64(value),
297 ],
298 )?;
299 Ok(())
300 }
301
302 /// The per-property numeric format string (`PropertyObject.NumericFormat`).
303 ///
304 /// A `printf`-style format that decides how
305 /// [`get_formatted_value`](Self::get_formatted_value) renders a number, so
306 /// the same stored value can display as decimal, hex, octal or binary. It
307 /// is presentation only, the underlying number is unchanged.
308 ///
309 /// Two departures from C: `%b` formats in binary, and a `$` placed straight
310 /// after the `%` strips trailing zeros after the decimal point. An empty
311 /// string restores the default format.
312 ///
313 /// # Errors
314 /// [`Error`] if the COM call fails or returns an unexpected type.
315 pub fn numeric_format(&self) -> Result<String, Error> {
316 Ok(self
317 .dispatch
318 .get(introspect::NUMERIC_FORMAT)?
319 .into_string()?)
320 }
321
322 /// Sets the numeric format string (`PropertyObject.NumericFormat`).
323 ///
324 /// # Errors
325 /// [`Error`] if the COM call fails.
326 pub fn set_numeric_format(&self, format: &str) -> Result<(), Error> {
327 self.dispatch
328 .put(introspect::NUMERIC_FORMAT, Value::Str(format.to_owned()))?;
329 Ok(())
330 }
331
332 /// Renders a property's value as display text (`GetFormattedValue`).
333 ///
334 /// `format` overrides the formatting for this call; pass an empty string to
335 /// use the default. Set `use_value_format_if_defined` to honour the
336 /// property's own [`numeric_format`](Self::numeric_format) instead.
337 /// `separator` joins array elements.
338 ///
339 /// Containers render as `...` and an empty reference as `Nothing`, so the
340 /// result is always displayable text rather than an error.
341 ///
342 /// # Errors
343 /// [`Error`] if the COM call fails or returns an unexpected type.
344 pub fn get_formatted_value(
345 &self,
346 lookup_string: &str,
347 options: i32,
348 format: &str,
349 use_value_format_if_defined: bool,
350 separator: &str,
351 ) -> Result<String, Error> {
352 Ok(self
353 .dispatch
354 .call(
355 introspect::GET_FORMATTED_VALUE,
356 &[
357 Value::Str(lookup_string.to_owned()),
358 Value::I32(options),
359 Value::Str(format.to_owned()),
360 Value::Bool(use_value_format_if_defined),
361 Value::Str(separator.to_owned()),
362 ],
363 )?
364 .into_string()?)
365 }
366
367 /// The object's name (`PropertyObject.Name`).
368 ///
369 /// A type definition must be named before it can be registered.
370 ///
371 /// # Errors
372 /// [`Error`] if the COM call fails or returns an unexpected type.
373 pub fn name(&self) -> Result<String, Error> {
374 Ok(self.dispatch.get(introspect::NAME)?.into_string()?)
375 }
376
377 /// Sets the object's name (`PropertyObject.Name`).
378 ///
379 /// # Errors
380 /// [`Error`] if the COM call fails.
381 pub fn set_name(&self, name: &str) -> Result<(), Error> {
382 self.dispatch
383 .put(introspect::NAME, Value::Str(name.to_owned()))?;
384 Ok(())
385 }
386
387 /// A type's version, as `major.minor.revision.build`
388 /// (`PropertyObject.TypeVersion`).
389 ///
390 /// Which field is bumped carries meaning: raising the lowest field signals
391 /// a change the engine can apply to existing instances silently, while
392 /// raising a higher one marks the change as deliberate.
393 ///
394 /// # Errors
395 /// [`Error`] if the COM call fails or returns an unexpected type.
396 pub fn type_version(&self) -> Result<String, Error> {
397 Ok(self.dispatch.get(introspect::TYPE_VERSION)?.into_string()?)
398 }
399
400 /// Sets a type's version (`PropertyObject.TypeVersion`).
401 ///
402 /// # Errors
403 /// [`Error`] if the COM call fails.
404 pub fn set_type_version(&self, version: &str) -> Result<(), Error> {
405 self.dispatch
406 .put(introspect::TYPE_VERSION, Value::Str(version.to_owned()))?;
407 Ok(())
408 }
409
410 /// The object's attributes (`PropertyObject.Attributes`).
411 ///
412 /// A property tree of its own, used for metadata that is not part of the
413 /// value, an enumeration's strictness flag, for instance.
414 ///
415 /// # Errors
416 /// [`Error`] if the COM call fails or returns an unexpected type.
417 pub fn attributes(&self) -> Result<Self, Error> {
418 Ok(Self::new(
419 self.dispatch.get(introspect::ATTRIBUTES)?.into_object()?,
420 ))
421 }
422
423 /// An enumeration's enumerators (`PropertyObject.Enumerators`).
424 ///
425 /// # Errors
426 /// [`Error`] if the COM call fails or returns an unexpected type.
427 pub fn enumerators(&self) -> Result<Self, Error> {
428 Ok(Self::new(
429 self.dispatch.get(introspect::ENUMERATORS)?.into_object()?,
430 ))
431 }
432
433 /// Replaces an enumeration's enumerators (`UpdateEnumerators`).
434 ///
435 /// Expects an array of containers, each holding `EnumeratorName` and
436 /// `EnumeratorValue`. Strictness rides on the array's attributes rather
437 /// than on an element.
438 ///
439 /// This only has an effect on a **registered** type definition; calling it
440 /// on the loose object that was inserted changes nothing. Every loaded
441 /// instance of the type is updated.
442 ///
443 /// # Errors
444 /// [`Error`] if the COM call fails or the argument is not a live object.
445 pub fn update_enumerators(&self, enumerators: &Self) -> Result<bool, Error> {
446 let handle = enumerators
447 .duplicate_dispatch()
448 .ok_or(Error::UnexpectedType {
449 expected: "a live property object",
450 actual: "a test fake with no COM identity",
451 })?;
452 Ok(self
453 .dispatch
454 .call(introspect::UPDATE_ENUMERATORS, &[Value::Object(handle)])?
455 .as_bool()?)
456 }
457
458 /// The display name of a value (`GetValueDisplayName`).
459 ///
460 /// For an enumeration this is the enumerator's name rather than its number.
461 ///
462 /// # Errors
463 /// [`Error`] if the COM call fails or returns an unexpected type.
464 pub fn get_value_display_name(
465 &self,
466 lookup_string: &str,
467 options: i32,
468 ) -> Result<String, Error> {
469 Ok(self
470 .dispatch
471 .call(
472 introspect::GET_VALUE_DISPLAY_NAME,
473 &[Value::Str(lookup_string.to_owned()), Value::I32(options)],
474 )?
475 .into_string()?)
476 }
477
478 /// An owned handle to the same object, for passing it back to the engine.
479 pub(crate) fn duplicate_dispatch(&self) -> Option<Box<dyn Dispatch>> {
480 self.dispatch.duplicate()
481 }
482
483 /// Evaluates an expression in this object's context (`Evaluate`).
484 ///
485 /// Superseded by [`evaluate_ex`](Self::evaluate_ex), which adds an options
486 /// argument. This form is kept because it is the member available on
487 /// engines from TestStand 2016, which the crate supports, a caller
488 /// targeting the whole range can use it without a version check.
489 ///
490 /// # Errors
491 /// [`Error`] if the expression is invalid or the COM call fails.
492 pub fn evaluate(&self, expression: &str) -> Result<Self, Error> {
493 Ok(Self::new(
494 self.dispatch
495 .call(introspect::EVALUATE, &[Value::Str(expression.to_owned())])?
496 .into_object()?,
497 ))
498 }
499
500 /// Evaluates an expression in this object's context (`EvaluateEx`).
501 ///
502 /// The object is the scope: the expression can name this property's
503 /// subproperties directly. The result comes back as a `PropertyObject`
504 /// holding whatever type the expression produced, so read it with the
505 /// accessor that matches, or with `to_value`.
506 ///
507 /// `Evaluate` is the obsolete form of this member; use this one.
508 ///
509 /// # Errors
510 /// [`Error`] if the expression is invalid or the COM call fails.
511 pub fn evaluate_ex(&self, expression: &str, options: i32) -> Result<Self, Error> {
512 Ok(Self::new(
513 self.dispatch
514 .call(
515 introspect::EVALUATE_EX,
516 &[Value::Str(expression.to_owned()), Value::I32(options)],
517 )?
518 .into_object()?,
519 ))
520 }
521
522 /// This property's type object (`PropertyObject.Type`).
523 ///
524 /// # Errors
525 /// [`Error`] if the COM call fails or returns an unexpected type.
526 pub fn property_type(&self) -> Result<crate::PropertyObjectType, Error> {
527 Ok(crate::PropertyObjectType::new(
528 self.dispatch.get(introspect::TYPE)?.into_object()?,
529 ))
530 }
531
532 /// A human-readable name for a property's type (`GetTypeDisplayString`).
533 ///
534 /// Obsolete in the engine; prefer
535 /// [`property_type`](Self::property_type) then `display_string`.
536 ///
537 /// This is the in-only route to identifying a type. `GetType` reports the
538 /// same thing in more detail but returns three of its five arguments by
539 /// reference, which the dispatch seam does not yet support.
540 ///
541 /// # Errors
542 /// [`Error`] if the COM call fails or returns an unexpected type.
543 pub fn get_type_display_string(
544 &self,
545 lookup_string: &str,
546 options: i32,
547 ) -> Result<String, Error> {
548 Ok(self
549 .dispatch
550 .call(
551 introspect::GET_TYPE_DISPLAY_STRING,
552 &[Value::Str(lookup_string.to_owned()), Value::I32(options)],
553 )?
554 .into_string()?)
555 }
556
557 /// A property's type as a flag set (`GetTypeFlags`).
558 ///
559 /// # Errors
560 /// [`Error`] if the COM call fails or returns an unexpected type.
561 pub fn get_type_flags(
562 &self,
563 lookup_string: &str,
564 options: i32,
565 ) -> Result<crate::PropertyValueTypeFlags, Error> {
566 let raw = self
567 .dispatch
568 .call(
569 introspect::GET_TYPE_FLAGS,
570 &[Value::Str(lookup_string.to_owned()), Value::I32(options)],
571 )?
572 .as_i32()?;
573 Ok(crate::PropertyValueTypeFlags::from_bits_retain(raw))
574 }
575
576 /// An array element by position (`GetPropertyObjectByOffset`).
577 ///
578 /// # Errors
579 /// [`Error`] if the COM call fails or returns an unexpected type.
580 pub fn get_property_object_by_offset(&self, offset: i32, options: i32) -> Result<Self, Error> {
581 Ok(Self::new(
582 self.dispatch
583 .call(
584 introspect::GET_PROPERTY_OBJECT_BY_OFFSET,
585 &[Value::I32(offset), Value::I32(options)],
586 )?
587 .into_object()?,
588 ))
589 }
590
591 /// Resizes an array property (`SetNumElements`).
592 ///
593 /// # Errors
594 /// [`Error`] if the COM call fails.
595 pub fn set_num_elements(&self, count: i32, options: i32) -> Result<(), Error> {
596 self.dispatch.call(
597 introspect::SET_NUM_ELEMENTS,
598 &[Value::I32(count), Value::I32(options)],
599 )?;
600 Ok(())
601 }
602
603 /// The number of elements in an array property (`GetNumElements`).
604 ///
605 /// # Errors
606 /// [`Error`] if the COM call fails or returns an unexpected type.
607 pub fn get_num_elements(&self) -> Result<i32, Error> {
608 Ok(self
609 .dispatch
610 .call(introspect::GET_NUM_ELEMENTS, &[])?
611 .as_i32()?)
612 }
613
614 /// Retrieves a nested `PropertyObject` by lookup path (`PropertyObject.GetPropertyObject`).
615 ///
616 /// # Errors
617 /// [`Error`] if the COM call fails or returns an unexpected type.
618 pub fn get_property_object(&self, lookup_string: &str, options: i32) -> Result<Self, Error> {
619 let dispatch = self
620 .dispatch
621 .call(
622 property_object::GET_PROPERTY_OBJECT,
623 &[Value::Str(lookup_string.to_string()), Value::I32(options)],
624 )?
625 .into_object()?;
626 Ok(Self::new(dispatch))
627 }
628
629 /// Attaches a nested `PropertyObject` by lookup path (`PropertyObject.SetPropertyObject`).
630 ///
631 /// # Errors
632 /// [`Error`] if the COM call fails.
633 pub fn set_property_object(
634 &self,
635 lookup_string: &str,
636 options: i32,
637 property_object_value: &Self,
638 ) -> Result<(), Error> {
639 let idispatch =
640 property_object_value
641 .as_dispatch()
642 .as_idispatch()
643 .ok_or(Error::UnexpectedType {
644 expected: "live COM dispatch object",
645 actual: "fake dispatch object",
646 })?;
647 let com_dispatch = rs_teststand_sys::ComDispatch::new(idispatch.clone());
648 self.dispatch.call(
649 property_object::SET_PROPERTY_OBJECT,
650 &[
651 Value::Str(lookup_string.to_string()),
652 Value::I32(options),
653 Value::Object(Box::new(com_dispatch)),
654 ],
655 )?;
656 Ok(())
657 }
658
659 /// Creates a new sub-property (`PropertyObject.NewSubProperty`).
660 ///
661 /// # Errors
662 /// [`Error`] if the COM call fails.
663 pub fn new_sub_property(
664 &self,
665 lookup_string: &str,
666 value_type: PropValType,
667 as_array: bool,
668 type_name: &str,
669 options: i32,
670 ) -> Result<(), Error> {
671 self.dispatch.call(
672 property_object::NEW_SUB_PROPERTY,
673 &[
674 Value::Str(lookup_string.to_string()),
675 Value::I32(value_type as i32),
676 Value::Bool(as_array),
677 Value::Str(type_name.to_string()),
678 Value::I32(options),
679 ],
680 )?;
681 Ok(())
682 }
683
684 /// Deletes a sub-property by lookup path (`PropertyObject.DeleteSubProperty`).
685 ///
686 /// # Errors
687 /// [`Error`] if the COM call fails.
688 pub fn delete_sub_property(&self, lookup_string: &str, options: i32) -> Result<(), Error> {
689 self.dispatch.call(
690 property_object::DELETE_SUB_PROPERTY,
691 &[Value::Str(lookup_string.to_string()), Value::I32(options)],
692 )?;
693 Ok(())
694 }
695
696 /// Clones a sub-property by lookup path (`PropertyObject.Clone`).
697 ///
698 /// # Errors
699 /// [`Error`] if the COM call fails or returns an unexpected type.
700 pub fn clone_property(&self, lookup_string: &str, options: i32) -> Result<Self, Error> {
701 let dispatch = self
702 .dispatch
703 .call(
704 property_object::CLONE,
705 &[Value::Str(lookup_string.to_string()), Value::I32(options)],
706 )?
707 .into_object()?;
708 Ok(Self::new(dispatch))
709 }
710}