touchportal_sdk/
settings.rs

1use crate::protocol::TouchPortalFromStr;
2use derive_builder::Builder;
3use serde::{Deserialize, Serialize};
4use std::collections::BTreeSet;
5
6#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
7#[builder(build_fn(validate = "Self::validate"))]
8#[serde(rename_all = "camelCase")]
9pub struct Setting {
10    /// This is the name of the settings in the settings overview.
11    ///
12    /// This is also the identifier.
13    #[builder(setter(into))]
14    pub(crate) name: String,
15
16    /// This will be the default value for your setting.
17    #[builder(setter(into))]
18    #[serde(rename = "default")]
19    pub(crate) initial: String,
20
21    /// This will specify what type of settings you can use. Currently you can only use "text" or "number".
22    #[serde(flatten)]
23    pub(crate) kind: SettingType,
24
25    /// An optional tooltip object allowing you to explain more about the setting.
26    ///
27    /// As of API 10 (Touch Portal 4.3) all tooltips will be shown as a description text above the
28    /// control in the plug-in settings. This is part of a redesign of the settings section.
29    #[builder(setter(strip_option), default)]
30    #[serde(skip_serializing_if = "Option::is_none")]
31    pub(crate) tooltip: Option<Tooltip>,
32}
33
34impl Setting {
35    pub fn builder() -> SettingBuilder {
36        SettingBuilder::default()
37    }
38}
39
40impl SettingBuilder {
41    fn validate(&self) -> Result<(), String> {
42        let initial = self.initial.as_ref().expect("initial is required");
43        let kind = self.kind.as_ref().expect("kind is required");
44
45        let max_length = match kind {
46            SettingType::Text(req) => req.max_length,
47            SettingType::Number(req) => req.max_length,
48            SettingType::Multiline(req) => req.max_length,
49            SettingType::File(_) => None,
50            SettingType::Folder(_) => None,
51            SettingType::Switch(_) => None,
52            SettingType::Choice(_) => None,
53        };
54
55        if let Some(max_length) = max_length
56            && initial.len() > max_length as usize
57        {
58            return Err(format!(
59                "initial value '{initial}' is longer \
60                    than allowed max length {max_length}"
61            ));
62        }
63
64        if let SettingType::Choice(c) = kind
65            && !c.choices.contains(initial.as_str())
66        {
67            return Err(format!(
68                "initial value '{initial}' is not among allowed choices"
69            ));
70        }
71
72        if let SettingType::Number(n) = kind {
73            match f64::destringify(initial) {
74                Ok(v) if n.min_value.is_some_and(|min| v < min) => {
75                    return Err(format!("initial value '{initial}' is below minimum value"));
76                }
77                Ok(v) if n.max_value.is_some_and(|max| v > max) => {
78                    return Err(format!("initial value '{initial}' is above maximum value"));
79                }
80                Ok(_) => {}
81                Err(_) => return Err(format!("initial value '{initial}' is not numeric")),
82            }
83        }
84
85        if let SettingType::Switch(_) = kind {
86            match bool::destringify(initial) {
87                Ok(_) => {}
88                _ => {
89                    return Err(format!(
90                        "initial value '{initial}' is not switch-y (must be On or Off)"
91                    ));
92                }
93            }
94        }
95
96        Ok(())
97    }
98}
99
100#[derive(Debug, Clone, Deserialize, Serialize)]
101#[non_exhaustive]
102#[serde(rename_all = "lowercase")]
103#[serde(tag = "type")]
104pub enum SettingType {
105    /// A normal text field settings item, can be used with maxLength, readOnly and isPassword
106    Text(TextSetting),
107
108    /// A number text field settings item.
109    Number(NumberSetting),
110
111    /// A file selector.
112    ///
113    /// Only available in API version 10 and above.
114    File(FileSetting),
115
116    /// A folder selector.
117    ///
118    /// Only available in API version 10 and above.
119    Folder(FolderSetting),
120
121    /// A multiline text field.
122    ///
123    /// Only available in API version 10 and above.
124    Multiline(MultilineSetting),
125
126    /// A switch for boolean settings.
127    ///
128    /// Only available in API version 10 and above.
129    Switch(SwitchSetting),
130
131    /// A choice box for preset options, can be used with choices.
132    ///
133    /// Only available in API version 10 and above.
134    Choice(ChoiceSetting),
135}
136
137#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct TextSetting {
140    /// This is the max amount of characters a text settings value can have.
141    #[builder(setter(strip_option), default)]
142    #[serde(skip_serializing_if = "Option::is_none")]
143    max_length: Option<u32>,
144
145    /// If set, will hide the characters from the input field.
146    ///
147    /// It will show dots instead. Please do know that communication between Touch Portal and the
148    /// plug-in is open text. This option is made so that random people will not be able to peek at
149    /// the password field. It is not secure.
150    #[builder(setter(strip_option), default)]
151    #[serde(skip_serializing_if = "Option::is_none")]
152    is_password: Option<bool>,
153
154    /// For some settings you do not want the user to edit them but you do want to share them.
155    ///
156    /// Using this switch will allow you to do so. Updating these setting values should be done
157    /// with the dynamic functions.
158    #[builder(setter(strip_option), default)]
159    #[serde(skip_serializing_if = "Option::is_none")]
160    read_only: Option<bool>,
161}
162
163impl TextSetting {
164    pub fn builder() -> TextSettingBuilder {
165        TextSettingBuilder::default()
166    }
167}
168
169#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
170#[serde(rename_all = "camelCase")]
171pub struct NumberSetting {
172    /// This is the max amount of characters a text settings value can have.
173    #[builder(setter(strip_option), default)]
174    #[serde(skip_serializing_if = "Option::is_none")]
175    max_length: Option<u32>,
176
177    /// If set, will hide the characters from the input field.
178    ///
179    /// It will show dots instead. Please do know that communication between Touch Portal and the
180    /// plug-in is open text. This option is made so that random people will not be able to peek at
181    /// the password field. It is not secure.
182    #[builder(setter(strip_option), default)]
183    #[serde(skip_serializing_if = "Option::is_none")]
184    is_password: Option<bool>,
185
186    /// For some settings you do not want the user to edit them but you do want to share them.
187    ///
188    /// Using this switch will allow you to do so. Updating these setting values should be done
189    /// with the dynamic functions.
190    #[builder(setter(strip_option), default)]
191    #[serde(skip_serializing_if = "Option::is_none")]
192    read_only: Option<bool>,
193
194    /// The minimum number value allowed for a number type setting.
195    #[builder(setter(strip_option), default)]
196    #[serde(skip_serializing_if = "Option::is_none")]
197    min_value: Option<f64>,
198
199    /// The maximum number value allowed for a number type setting.
200    #[builder(setter(strip_option), default)]
201    #[serde(skip_serializing_if = "Option::is_none")]
202    max_value: Option<f64>,
203}
204
205impl NumberSetting {
206    pub fn builder() -> NumberSettingBuilder {
207        NumberSettingBuilder::default()
208    }
209}
210
211#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
212#[serde(rename_all = "camelCase")]
213pub struct FileSetting {}
214
215impl FileSetting {
216    pub fn builder() -> FileSettingBuilder {
217        FileSettingBuilder::default()
218    }
219}
220
221#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
222#[serde(rename_all = "camelCase")]
223pub struct FolderSetting {}
224
225impl FolderSetting {
226    pub fn builder() -> FolderSettingBuilder {
227        FolderSettingBuilder::default()
228    }
229}
230
231#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
232#[serde(rename_all = "camelCase")]
233pub struct MultilineSetting {
234    /// This is the max amount of characters a text settings value can have.
235    #[builder(setter(strip_option), default)]
236    #[serde(skip_serializing_if = "Option::is_none")]
237    max_length: Option<u32>,
238
239    /// For some settings you do not want the user to edit them but you do want to share them.
240    ///
241    /// Using this switch will allow you to do so. Updating these setting values should be done
242    /// with the dynamic functions.
243    #[builder(setter(strip_option), default)]
244    #[serde(skip_serializing_if = "Option::is_none")]
245    read_only: Option<bool>,
246}
247
248impl MultilineSetting {
249    pub fn builder() -> MultilineSettingBuilder {
250        MultilineSettingBuilder::default()
251    }
252}
253
254#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
255#[serde(rename_all = "camelCase")]
256pub struct SwitchSetting {}
257
258impl SwitchSetting {
259    pub fn builder() -> SwitchSettingBuilder {
260        SwitchSettingBuilder::default()
261    }
262}
263
264#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
265#[serde(rename_all = "camelCase")]
266pub struct ChoiceSetting {
267    /// These are all the options the user can select for the setting.
268    #[builder(setter(each(name = "choice", into)))]
269    pub(crate) choices: BTreeSet<String>,
270}
271
272impl ChoiceSetting {
273    pub fn builder() -> ChoiceSettingBuilder {
274        ChoiceSettingBuilder::default()
275    }
276}
277
278#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
279#[serde(rename_all = "camelCase")]
280pub struct Tooltip {
281    /// This is the title for the tooltip.
282    ///
283    /// If this is not added or is left empty, the title will not be shown in the tooltip.
284    #[builder(setter(into, strip_option), default)]
285    #[serde(skip_serializing_if = "Option::is_none")]
286    pub(crate) title: Option<String>,
287
288    /// This is the body for the tooltip.
289    #[builder(setter(into))]
290    pub(crate) body: String,
291
292    /// This is the url to the documentation if this is available.
293    ///
294    /// If this is empty, no link to documentation is added in the tooltip.
295    #[builder(setter(into, strip_option), default)]
296    #[serde(skip_serializing_if = "Option::is_none")]
297    pub(crate) doc_url: Option<String>,
298}
299
300impl Tooltip {
301    pub fn builder() -> TooltipBuilder {
302        TooltipBuilder::default()
303    }
304}
305
306#[test]
307fn serialize_example_setting() {
308    assert_eq!(
309        serde_json::to_value(
310            Setting::builder()
311                .name("Age")
312                .initial("23")
313                .kind(SettingType::Number(
314                    NumberSetting::builder()
315                        .max_length(20)
316                        .is_password(false)
317                        .min_value(0.0)
318                        .max_value(120.0)
319                        .read_only(false)
320                        .build()
321                        .unwrap()
322                ))
323                .build()
324                .unwrap()
325        )
326        .unwrap(),
327        serde_json::json! {{
328          "name":"Age",
329          "default":"23",
330          "type":"number",
331          "maxLength":20,
332          "isPassword":false,
333          "minValue":0.,
334          "maxValue":120.,
335          "readOnly":false
336        }}
337    );
338}
339
340#[test]
341fn serialize_example_setting_with_tooltip() {
342    assert_eq!(
343        serde_json::to_value(
344            Setting::builder()
345                .name("Age")
346                .initial("23")
347                .kind(SettingType::Number(
348                    NumberSetting::builder()
349                        .max_length(20)
350                        .is_password(false)
351                        .min_value(0.0)
352                        .max_value(120.0)
353                        .read_only(false)
354                        .build()
355                        .unwrap()
356                ))
357                .tooltip(
358                    Tooltip::builder()
359                        .title("Toolstip")
360                        .body(
361                            "Learn more about how tooltips work in the Touch Portal API documentation."
362                        )
363                        .doc_url(
364                            "https://www.touch-portal.com/api/v2/index.php?section=description_file_settings"
365                        )
366                        .build()
367                        .unwrap()
368                )
369                .build()
370                .unwrap()
371        )
372        .unwrap(),
373        serde_json::json! {{
374          "name":"Age",
375          "default":"23",
376          "type":"number",
377          "maxLength":20,
378          "isPassword":false,
379          "minValue":0.,
380          "maxValue":120.,
381          "readOnly":false,
382          "tooltip":{
383            "title":"Toolstip",
384            "body":"Learn more about how tooltips work in the Touch Portal API documentation.",
385            "docUrl":"https://www.touch-portal.com/api/v2/index.php?section=description_file_settings"
386          }
387        }}
388    );
389}