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}