Skip to main content

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}