touchportal_sdk/
events.rs

1use super::PluginCategory;
2use derive_builder::Builder;
3use indexmap::IndexSet;
4use serde::{Deserialize, Serialize};
5
6/// In Touch Portal there are events which will be triggered when a certain state changes.
7///
8/// You can create events for the plugin as well. These events can be triggered when a linked state
9/// is changed.
10///
11/// Please note: when a user adds an event belonging to a plugin, it will create a local copy of
12/// the event and saves it along with the event. This means that if you change something in your
13/// event the users need to remove their instance of that event and re-add it to be able to use the
14/// new additions.
15#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
16#[builder(build_fn(validate = "Self::validate"))]
17#[serde(rename_all = "camelCase")]
18pub struct Event {
19    /// This is the id of the event.
20    ///
21    /// When the event is triggered, Touch Portal will send this information to the plugin with this id.
22    #[builder(setter(into))]
23    pub(crate) id: String,
24
25    /// This is the name in the action category list.
26    #[builder(setter(into))]
27    pub(crate) name: String,
28
29    /// This is the text the action will show in the user generated action list.
30    ///
31    /// The `$val` location will be changed with a dropdown holding the choices that the user can
32    /// make for the status.
33    ///
34    /// The `$compare` location will be changed depending on the value of
35    /// [`EventTextConfiguration::compare_with`], and only works for text fields (for now).
36    /// Requires `$val` to be included.
37    #[builder(setter(into))]
38    pub(crate) format: String,
39
40    /// Currently the only option here is "communicate" which indicates that the value will be
41    /// communicated through the sockets.
42    #[builder(setter(skip), default)]
43    #[serde(rename = "type")]
44    _type: EventType,
45
46    /// If you are not associated this event with a state (through `value_state_id`), it does not
47    /// matter what you set this to.
48    #[serde(flatten)]
49    pub(crate) value: EventValueType,
50
51    /// Reference to a state.
52    ///
53    /// When this states changes, this event will be evaluated and possibly triggered if the
54    /// condition is correct. Can be empty but is mandatory.
55    #[builder(setter(into), default)]
56    pub(crate) value_state_id: String,
57
58    /// This attribute allows you to connect this event to a specified subcategory id.
59    ///
60    /// This event will then be shown in Touch Portals Action selection list attached to that
61    /// subcategory instead of the main parent category.
62    #[builder(setter(into, strip_option), default)]
63    #[serde(skip_serializing_if = "Option::is_none")]
64    sub_category_id: Option<String>,
65
66    /// Array of all Local State objects related to this event.
67    ///
68    /// These can be selected by the user only when the event is used and added. If not added, the
69    /// local states will not be shown in the state selector popups.
70    ///
71    /// Note that local states are not dependent on the [`EventValueType`].
72    ///
73    /// Only available in API version 10 and above.
74    #[serde(rename = "localstates")]
75    #[serde(skip_serializing_if = "Vec::is_empty")]
76    #[builder(setter(each(name = "local_state")), default)]
77    pub(crate) local_states: Vec<LocalState>,
78}
79
80impl Event {
81    pub fn builder() -> EventBuilder {
82        EventBuilder::default()
83    }
84}
85
86impl EventBuilder {
87    fn validate(&self) -> Result<(), String> {
88        let fmt = self.format.as_ref().expect("format is required");
89        if fmt.contains("$compare") && !fmt.contains("$val") {
90            return Err(format!(
91                "format for event {} has $compare but no $val",
92                self.id.as_ref().expect("id is required")
93            ));
94        }
95
96        Ok(())
97    }
98}
99
100#[derive(Debug, Clone, Deserialize, Serialize, Default)]
101#[non_exhaustive]
102#[serde(rename_all = "lowercase")]
103enum EventType {
104    #[default]
105    Communicate,
106}
107
108#[derive(Debug, Clone, Deserialize, Serialize)]
109#[non_exhaustive]
110#[serde(rename_all = "lowercase")]
111#[serde(tag = "valueType")]
112pub enum EventValueType {
113    /// Indicates that the type of event will be an dropdown with predefined values.
114    Choice(EventChoiceValue),
115
116    /// This will check whether the state is the same as the user specified value in the text box.
117    Text(EventTextConfiguration),
118}
119
120#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
121#[serde(rename_all = "camelCase")]
122pub struct EventChoiceValue {
123    /// These are all the options the user can select in the event.
124    #[builder(setter(each(name = "choice", into)))]
125    #[serde(rename = "valueChoices")]
126    pub(crate) choices: IndexSet<String>,
127}
128
129impl EventChoiceValue {
130    pub fn builder() -> EventChoiceValueBuilder {
131        EventChoiceValueBuilder::default()
132    }
133}
134
135#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
136#[serde(rename_all = "camelCase")]
137pub struct EventTextConfiguration {
138    /// What options the `$compare` token in the format for a text field should give.
139    #[builder(setter(strip_option), default)]
140    #[serde(skip_serializing_if = "Option::is_none")]
141    #[serde(rename = "compareOptions")]
142    pub(crate) compare_with: Option<CompareMethod>,
143}
144
145impl EventTextConfiguration {
146    pub fn builder() -> EventTextConfigurationBuilder {
147        EventTextConfigurationBuilder::default()
148    }
149}
150
151#[derive(Debug, Clone, Deserialize, Serialize, Default)]
152pub enum CompareMethod {
153    /// Old-fashioned string compare.
154    ///
155    /// Renders as "is equal".
156    #[serde(rename = "none")]
157    #[default]
158    StringEq,
159
160    /// Lets the user chose `!=` or `==`.
161    ///
162    /// Renders as a dropdown with the options "is equal to" and "is NOT equal to"
163    #[serde(rename = "choice")]
164    IsOrIsNot,
165
166    /// Lets the user make more complicated string comparisons.
167    ///
168    /// Renders as a dropdown with options like "contains", "begins with", "case insensitive
169    /// compared", etc.
170    #[serde(rename = "string")]
171    ExtendedString,
172
173    /// Lets the user make numeric comparisons on the operator.
174    ///
175    /// Renders a dropdown that has `<` and `>` in addition to equal and not equal.
176    #[serde(rename = "number")]
177    Number,
178}
179
180/// The local states object represents the representation and visualisation within Touch Portal.
181///
182/// The id is the reference when used as a tag in text. The actual setting of the local states
183/// object when the event is triggered are described in the communication section.
184#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
185#[serde(rename_all = "camelCase")]
186pub struct LocalState {
187    /// This id of the local state.
188    #[builder(setter(into))]
189    pub(crate) id: String,
190
191    /// This name of the local state.
192    #[builder(setter(into))]
193    pub(crate) name: String,
194
195    /// The parent category the local state belongs to.
196    #[builder(setter(strip_option), default)]
197    #[serde(skip_serializing_if = "Option::is_none")]
198    parent_category: Option<PluginCategory>,
199}
200
201impl LocalState {
202    pub fn builder() -> LocalStateBuilder {
203        LocalStateBuilder::default()
204    }
205}
206
207#[test]
208fn serialize_example_event() {
209    assert_eq!(
210        serde_json::to_value(
211            Event::builder()
212                .id("event002")
213                .name("On breakfast eating")
214                .format("When we eat $val as breakfast")
215                .value(EventValueType::Choice(
216                    EventChoiceValue::builder()
217                        .choice("Apple")
218                        .choice("Pears")
219                        .choice("Grapes")
220                        .choice("Bananas")
221                        .build()
222                        .unwrap()
223                ))
224                .value_state_id("fruit")
225                .build()
226                .unwrap()
227        )
228        .unwrap(),
229        serde_json::json! {{
230          "id":"event002",
231          "name":"On breakfast eating",
232          "format":"When we eat $val as breakfast",
233          "type":"communicate",
234          "valueType":"choice",
235          "valueChoices": [
236            "Apple",
237            "Pears",
238            "Grapes",
239            "Bananas",
240          ],
241          "valueStateId":"fruit"
242        }}
243    );
244}