Skip to main content

template_manage_complex/
template_manage_complex.rs

1//! Example: build reusable templates and stamp copies into a sequence file.
2//!
3//! ```text
4//! cargo run --example template_manage_complex
5//! ```
6//!
7//! A template in TestStand is not a type of its own: it is an ordinary
8//! `PropertyObject` kept somewhere a program can find it again. The station has
9//! a file for exactly that, `Engine.GetTemplatesFile`, behind the editor's
10//! Insert menus, but nothing stops a program from keeping its own, which is
11//! what this example does: an in-memory array container holding a step, a
12//! sequence and a variable prototype.
13//!
14//! The interesting part is the copying. `Clone` is a `PropertyObject` member,
15//! so every copy arrives on the `PropertyObject` interface, and `Step` and
16//! `Sequence` share none of its dispatch identifiers. `insert_step_from_template`
17//! and `insert_sequence_from_template` fold that into one call: they clone,
18//! insert, and hand back the copy on the interface it belongs on.
19
20use rs_teststand::{
21    Engine, Error, GetSeqFileOptions, GetTemplatesFileOptions, PropValType, PropertyObject,
22    PropertyOptions, SequenceFile, StepGroup,
23};
24
25/// The sequence every new file starts with.
26const MAIN_SEQUENCE: &str = "MainSequence";
27/// An empty adapter key: let the step type choose its own adapter.
28const NO_ADAPTER: &str = "";
29/// Where the templates live in this example's own group, by name.
30const STEP_TEMPLATE: &str = "My_Custom_Step_Template";
31const SEQUENCE_TEMPLATE: &str = "My_Custom_Sequence_Template";
32const VARIABLE_TEMPLATE: &str = "My_Custom_Variable_Template";
33
34/// Reports what the station itself offers, without disturbing it.
35///
36/// The file's tree is `Data` → `Root` → one array per kind of template
37/// (`Steps`, `Variables`, `Sequences`). Those arrays are empty on a station
38/// nobody has saved a template on, which is the normal state and not a
39/// prerequisite for anything below.
40fn describe_station_templates(engine: &Engine) -> Result<(), Error> {
41    let templates_file = engine.get_templates_file(GetTemplatesFileOptions::LOAD_IF_NOT_LOADED)?;
42    let root = templates_file
43        .data()?
44        .get_property_object("Root", PropertyOptions::NONE.bits())?;
45
46    println!("Station templates file: {}", templates_file.path()?);
47    for index in 0..root.get_num_elements()? {
48        let category = root.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
49        println!(
50            "  {}: {} template(s)",
51            category.name()?,
52            category.get_num_elements()?
53        );
54    }
55    Ok(())
56}
57
58/// An empty array container to keep prototypes in.
59fn new_template_group(engine: &Engine) -> Result<PropertyObject, Error> {
60    engine.new_property_object(
61        PropValType::Container,
62        true,
63        "",
64        PropertyOptions::NONE.bits(),
65    )
66}
67
68/// Appends a detached copy of a prototype to the group.
69///
70/// The copy matters: the group must not share the object the caller keeps
71/// editing, or adding a second template would change the first.
72fn append_template(group: &PropertyObject, template: &PropertyObject) -> Result<(), Error> {
73    let index = group.get_num_elements()?;
74    group.set_num_elements(index + 1, PropertyOptions::NONE.bits())?;
75    group.set_property_object(
76        &format!("[{index}]"),
77        PropertyOptions::NONE.bits(),
78        &template.clone_property("", PropertyOptions::NONE.bits())?,
79    )
80}
81
82/// Finds a prototype in the group by name.
83fn find_template(group: &PropertyObject, name: &str) -> Result<Option<PropertyObject>, Error> {
84    for index in 0..group.get_num_elements()? {
85        let candidate = group.get_property_object_by_offset(index, PropertyOptions::NONE.bits())?;
86        if candidate.name()? == name {
87            return Ok(Some(candidate));
88        }
89    }
90    Ok(None)
91}
92
93/// Turns "not found" into an error naming the template that is missing.
94fn require(found: Option<PropertyObject>, name: &'static str) -> Result<PropertyObject, Error> {
95    found.ok_or(Error::UnexpectedType {
96        expected: name,
97        actual: "no template of that name in the group",
98    })
99}
100
101/// A configured Statement step, taken as a property tree so it can be cloned.
102fn step_template(engine: &Engine) -> Result<PropertyObject, Error> {
103    let step = engine.new_step(NO_ADAPTER, "Statement")?;
104    step.set_name(STEP_TEMPLATE)?;
105    step.set_post_expression(r#"Locals.Result = "Hello from step template!""#)?;
106    step.as_property_object()
107}
108
109/// A whole sequence, one step included, as a single prototype.
110fn sequence_template(engine: &Engine) -> Result<PropertyObject, Error> {
111    let sequence = engine.new_sequence()?;
112    sequence.set_name(SEQUENCE_TEMPLATE)?;
113
114    let step = engine.new_step(NO_ADAPTER, "Statement")?;
115    step.set_name("Inside_Sequence_Template")?;
116    sequence.insert_step(&step, 0, StepGroup::Main)?;
117
118    sequence.as_property_object()
119}
120
121/// A string variable carrying its default value.
122fn variable_template(engine: &Engine) -> Result<PropertyObject, Error> {
123    let variable =
124        engine.new_property_object(PropValType::String, false, "", PropertyOptions::NONE.bits())?;
125    variable.set_name(VARIABLE_TEMPLATE)?;
126    variable.set_val_string("", PropertyOptions::NONE.bits(), "Template Variable Value")?;
127    Ok(variable)
128}
129
130/// Stamps one copy of each template into a file that is already on disk.
131fn apply_templates(
132    sequence_file: &SequenceFile,
133    step: &PropertyObject,
134    sequence: &PropertyObject,
135    variable: &PropertyObject,
136) -> Result<(), Error> {
137    let main_sequence = sequence_file.get_sequence_by_name(MAIN_SEQUENCE)?;
138
139    // Inserting returns the copy on the Step interface, so it can be renamed
140    // and re-identified straight away. Without a new step ID the copy would go
141    // on claiming the prototype's identity.
142    let inserted_step = main_sequence.insert_step_from_template(step, 0, StepGroup::Main)?;
143    inserted_step.set_name("Step From Template")?;
144    inserted_step.create_new_unique_step_id()?;
145    println!("  step:     {}", inserted_step.name()?);
146
147    let inserted_sequence = sequence_file.insert_sequence_from_template(sequence)?;
148    inserted_sequence.create_new_unique_step_ids()?;
149    println!(
150        "  sequence: {} ({} step(s))",
151        inserted_sequence.name()?,
152        inserted_sequence.get_num_steps(StepGroup::Main)?
153    );
154
155    // A variable is the easy case: Locals is a property tree, so a clone drops
156    // straight in under the template's own name.
157    let locals = main_sequence.locals()?;
158    locals.set_property_object(
159        &variable.name()?,
160        PropertyOptions::INSERT_IF_MISSING.bits(),
161        &variable.clone_property("", PropertyOptions::NONE.bits())?,
162    )?;
163    println!(
164        "  variable: {} = {:?}",
165        variable.name()?,
166        locals.get_val_string(&variable.name()?, PropertyOptions::NONE.bits())?
167    );
168
169    // A file that does not believe it changed will not write anything.
170    sequence_file
171        .as_property_object_file()?
172        .inc_change_count()?;
173    Ok(())
174}
175
176fn main() -> Result<(), Box<dyn std::error::Error>> {
177    let engine = Engine::new()?;
178    describe_station_templates(&engine)?;
179
180    println!("\nBuilding an in-memory template group...");
181    let group = new_template_group(&engine)?;
182    append_template(&group, &step_template(&engine)?)?;
183    append_template(&group, &sequence_template(&engine)?)?;
184    append_template(&group, &variable_template(&engine)?)?;
185    println!("  {} template(s) stored", group.get_num_elements()?);
186
187    let step = require(find_template(&group, STEP_TEMPLATE)?, STEP_TEMPLATE)?;
188    let sequence = require(find_template(&group, SEQUENCE_TEMPLATE)?, SEQUENCE_TEMPLATE)?;
189    let variable = require(find_template(&group, VARIABLE_TEMPLATE)?, VARIABLE_TEMPLATE)?;
190
191    // Saving first, then reopening, is deliberate: templates are worth having
192    // because they are applied to files a program did not build in this run.
193    let path = std::env::temp_dir().join("rs_teststand_from_templates.seq");
194    let path = path.to_string_lossy().into_owned();
195    engine.new_sequence_file()?.save(&path)?;
196
197    println!("\nApplying templates to the saved file...");
198    let target = engine.get_sequence_file_ex(
199        &path,
200        GetSeqFileOptions::DO_NOT_RUN_LOAD_CALLBACK,
201        rs_teststand::ConflictHandler::Error,
202    )?;
203    apply_templates(&target, &step, &sequence, &variable)?;
204    target.save(&path)?;
205    println!("\nSaved to {path}");
206
207    engine.release_sequence_file_ex(target, PropertyOptions::NONE.bits())?;
208    Ok(())
209}