touchportal_sdk/
data.rs

1use derive_builder::Builder;
2use hex_color::HexColor;
3use indexmap::IndexSet;
4use serde::{Deserialize, Serialize};
5
6/// As a plug-in developer you can augment your actions with additional data that the user has to
7/// fill in.
8///
9/// It uses the same structures as the native actions from Touch Portal itself.
10///
11/// You can use this for both static actions as dynamic actions. The user will have to specify
12/// values for the given data field within Touch Portal.
13#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
14pub struct Data {
15    /// This is the id of the data field.
16    ///
17    /// Touch Portal will use this for communicating the values or to place the values in the
18    /// result.
19    #[builder(setter(into))]
20    pub(crate) id: String,
21
22    #[serde(flatten)]
23    pub(crate) format: DataFormat,
24}
25
26impl Data {
27    pub fn builder() -> DataBuilder {
28        DataBuilder::default()
29    }
30}
31
32#[derive(Debug, Clone, Deserialize, Serialize)]
33#[serde(tag = "type")]
34#[serde(rename_all = "camelCase")]
35#[non_exhaustive]
36pub enum DataFormat {
37    /// A data type that accepts a string
38    Text(TextData),
39    /// A data type that accepts a number
40    Number(NumberData),
41    /// A data type that accepts a true or false value
42    Switch(SwitchData),
43    /// A data type that accepts a string where a collection of strings can be chosen from
44    Choice(ChoiceData),
45    /// A data type that represents a file which the user can pick with a file chooser
46    File(FileData),
47    /// A data type that represents a folder which the user can pick with a folder chooser
48    Folder(FolderData),
49    /// A data type that represents a color which the user can pick with a color chooser.
50    ///
51    /// This value must be in a the format `#RRGGBBAA`.
52    Color(ColorData),
53    /// A data type that represents a field for the user to specify the lower bound of the slider
54    /// range.
55    ///
56    /// The amount of decimals will also specify the precision. For example, if the user sets the
57    /// lower bound to 1, all values will be whole numbers. If the value is set to 1.0 it will
58    /// return connector values times 10, if the value is set to 1.00 it will return connector
59    /// values times 100. The plug-in is responsible of dividing the value to the proper range
60    /// before it is used. Connectors are only capable of sending integer data.
61    ///
62    /// If `UpperBound` is also set, both fields will be checked for precision. The higher
63    /// precision will be used. A range between 1 and 5.0 means it will use the 5.0 for the
64    /// precision.
65    ///
66    /// Only available for connectors.
67    ///
68    /// Only available in API version 10 and above.
69    LowerBound(BoundData),
70    /// A data type that represents a field for the user to specify the upper bound of the slider
71    /// range.
72    ///
73    /// The amount of decimals will also specify the precision. For example, if the user sets the
74    /// upper bound to 1, all values of the connector will be send as normal but will be translated
75    /// to the range specified. If the value is set to 1.0 it will return connector values times
76    /// 10, if the value is set to 1.00 it will return connector values times 100. The plug-in is
77    /// responsible of dividing the value to the proper range before it is used. Connectors are
78    /// only capable of sending integer data.
79    ///
80    /// If `LowerBound` is also set, both fields will be checked for precision. The higher
81    /// precision will be used. A range between 1 and 5.0 means it will use the 5.0 for the
82    /// precision.
83    ///
84    /// Only available for connectors.
85    ///
86    /// Only available in API version 10 and above.
87    UpperBound(BoundData),
88    // TODO: valueStore
89}
90
91#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
92#[serde(rename_all = "camelCase")]
93pub struct TextData {
94    /// This is the default value the data object has.
95    #[builder(setter(into), default)]
96    #[serde(rename = "default")]
97    #[doc(alias = "default")]
98    pub(crate) initial: String,
99}
100
101impl TextData {
102    pub fn builder() -> TextDataBuilder {
103        TextDataBuilder::default()
104    }
105}
106
107fn bool_is_true(b: &bool) -> bool {
108    *b
109}
110
111#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
112#[builder(build_fn(validate = "Self::validate"))]
113#[serde(rename_all = "camelCase")]
114pub struct NumberData {
115    /// This is the default value the data object has.
116    #[serde(rename = "default")]
117    #[doc(alias = "default")]
118    pub(crate) initial: f64,
119
120    /// This field tells the system whether this data field should allow decimals in the number.
121    ///
122    /// The default is `true`.
123    #[builder(default = true)]
124    #[serde(skip_serializing_if = "bool_is_true")]
125    pub(crate) allow_decimals: bool,
126
127    /// This is the lowest number that will be accepted.
128    ///
129    /// The user will get a message to correct the data if it is lower and the new value will be
130    /// rejected.
131    #[serde(skip_serializing_if = "Option::is_none")]
132    #[builder(setter(into, strip_option), default)]
133    pub(crate) min_value: Option<f64>,
134
135    /// This is the highest number that will be accepted.
136    ///
137    /// The user will get a message to correct the data if it is higher and the new value will be
138    /// rejected.
139    #[serde(skip_serializing_if = "Option::is_none")]
140    #[builder(setter(into, strip_option), default)]
141    pub(crate) max_value: Option<f64>,
142}
143
144impl NumberData {
145    pub fn builder() -> NumberDataBuilder {
146        NumberDataBuilder::default()
147    }
148}
149
150impl NumberDataBuilder {
151    fn validate(&self) -> Result<(), String> {
152        let initial = self.initial.expect("initial is required");
153        let min = self.min_value.flatten();
154        let max = self.max_value.flatten();
155
156        if let Some(min_val) = min
157            && initial < min_val
158        {
159            if let Some(max_val) = max {
160                return Err(format!(
161                    "initial value {} is outside the allowed range [{}, {}]",
162                    initial, min_val, max_val
163                ));
164            } else {
165                return Err(format!(
166                    "initial value {} is below the minimum allowed value {}",
167                    initial, min_val
168                ));
169            }
170        }
171
172        if let Some(max_val) = max
173            && initial > max_val
174        {
175            if let Some(min_val) = min {
176                return Err(format!(
177                    "initial value {} is outside the allowed range [{}, {}]",
178                    initial, min_val, max_val
179                ));
180            } else {
181                return Err(format!(
182                    "initial value {} is above the maximum allowed value {}",
183                    initial, max_val
184                ));
185            }
186        }
187
188        Ok(())
189    }
190}
191
192#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
193#[serde(rename_all = "camelCase")]
194pub struct SwitchData {
195    /// This is the default value the data object has.
196    #[serde(rename = "default")]
197    #[doc(alias = "default")]
198    pub(crate) initial: bool,
199}
200
201impl SwitchData {
202    pub fn builder() -> SwitchDataBuilder {
203        SwitchDataBuilder::default()
204    }
205}
206
207#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
208#[serde(rename_all = "camelCase")]
209pub struct ChoiceData {
210    /// This is the default value the data object has.
211    #[builder(setter(into))]
212    #[serde(rename = "default")]
213    #[doc(alias = "default")]
214    pub(crate) initial: String,
215
216    #[builder(setter(each(name = "choice", into)))]
217    pub(crate) value_choices: Vec<String>,
218}
219
220impl ChoiceData {
221    pub fn builder() -> ChoiceDataBuilder {
222        ChoiceDataBuilder::default()
223    }
224}
225
226#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
227#[serde(rename_all = "camelCase")]
228pub struct FileData {
229    /// This is the default value the data object has.
230    #[builder(setter(into))]
231    #[serde(rename = "default")]
232    #[doc(alias = "default")]
233    pub(crate) initial: String,
234
235    /// This is a collection of extensions allowed to open.
236    ///
237    /// eg: `"extensions": ["*.jpg","*.png"]`
238    #[builder(setter(each(name = "extension")), default)]
239    #[serde(skip_serializing_if = "IndexSet::is_empty")]
240    pub(crate) extensions: IndexSet<String>,
241}
242
243impl FileData {
244    pub fn builder() -> FileDataBuilder {
245        FileDataBuilder::default()
246    }
247}
248
249#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
250#[serde(rename_all = "camelCase")]
251pub struct FolderData {
252    /// This is the default value the data object has.
253    #[builder(setter(into))]
254    #[serde(rename = "default")]
255    #[doc(alias = "default")]
256    pub(crate) initial: String,
257}
258
259impl FolderData {
260    pub fn builder() -> FolderDataBuilder {
261        FolderDataBuilder::default()
262    }
263}
264
265#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
266#[serde(rename_all = "camelCase")]
267pub struct ColorData {
268    /// This is the default value the data object has.
269    #[builder(setter(into))]
270    #[serde(rename = "default")]
271    #[doc(alias = "default")]
272    pub(crate) initial: HexColor,
273}
274
275impl ColorData {
276    pub fn builder() -> ColorDataBuilder {
277        ColorDataBuilder::default()
278    }
279}
280
281#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
282#[serde(rename_all = "camelCase")]
283pub struct BoundData {
284    /// This is the default value the data object has.
285    #[serde(rename = "default")]
286    #[doc(alias = "default")]
287    pub(crate) initial: i64,
288
289    /// This is the lowest number that will be accepted.
290    ///
291    /// The user will get a message to correct the data if it is lower and the new value will be
292    /// rejected.
293    #[serde(skip_serializing_if = "Option::is_none")]
294    #[builder(setter(into, strip_option), default)]
295    pub(crate) min_value: Option<i64>,
296
297    /// This is the highest number that will be accepted.
298    ///
299    /// The user will get a message to correct the data if it is higher and the new value will be
300    /// rejected.
301    #[serde(skip_serializing_if = "Option::is_none")]
302    #[builder(setter(into, strip_option), default)]
303    pub(crate) max_value: Option<i64>,
304}
305
306impl BoundData {
307    pub fn builder() -> BoundDataBuilder {
308        BoundDataBuilder::default()
309    }
310}
311
312#[test]
313fn serialize_example_action_data_text() {
314    assert_eq!(
315        serde_json::to_value(
316            Data::builder()
317                .id("actiondata001")
318                .format(DataFormat::Text(
319                    TextData::builder().initial("any text").build().unwrap()
320                ))
321                .build()
322                .unwrap()
323        )
324        .unwrap(),
325        serde_json::json! {{
326          "id":"actiondata001",
327          "type":"text",
328          "default":"any text"
329        }}
330    );
331}
332
333#[test]
334fn serialize_example_action_data_number() {
335    assert_eq!(
336        serde_json::to_value(
337            Data::builder()
338                .id("first")
339                .format(DataFormat::Number(
340                    NumberData::builder()
341                        .initial(200.)
342                        .min_value(100.)
343                        .max_value(350.)
344                        .build()
345                        .unwrap()
346                ))
347                .build()
348                .unwrap()
349        )
350        .unwrap(),
351        serde_json::json! { {
352          "id":"first",
353          "type":"number",
354          "default":200.0,
355          "minValue":100.0,
356          "maxValue":350.0,
357        }}
358    );
359}
360
361#[test]
362fn serialize_example_action_data_choice() {
363    assert_eq!(
364        serde_json::to_value(
365            Data::builder()
366                .id("second")
367                .format(DataFormat::Choice(
368                    ChoiceData::builder()
369                        .initial("200")
370                        .choice("200")
371                        .choice("400")
372                        .choice("600")
373                        .choice("800")
374                        .build()
375                        .unwrap()
376                ))
377                .build()
378                .unwrap()
379        )
380        .unwrap(),
381        serde_json::json! {{
382          "id":"second",
383          "type":"choice",
384          "default":"200",
385          "valueChoices": [
386              "200",
387              "400",
388              "600",
389              "800"
390          ]
391        }}
392    );
393}
394
395#[test]
396fn serialize_example_action_data_switch() {
397    assert_eq!(
398        serde_json::to_value(
399            Data::builder()
400                .id("actiondata003")
401                .format(DataFormat::Switch(
402                    SwitchData::builder().initial(true).build().unwrap()
403                ))
404                .build()
405                .unwrap()
406        )
407        .unwrap(),
408        serde_json::json! {{
409          "id":"actiondata003",
410          "type":"switch",
411          "default":true
412        }}
413    );
414}