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 #[builder(setter(into))]
14 pub(crate) name: String,
15
16 #[builder(setter(into))]
18 #[serde(rename = "default")]
19 pub(crate) initial: String,
20
21 #[serde(flatten)]
23 pub(crate) kind: SettingType,
24
25 #[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 Text(TextSetting),
107
108 Number(NumberSetting),
110
111 File(FileSetting),
115
116 Folder(FolderSetting),
120
121 Multiline(MultilineSetting),
125
126 Switch(SwitchSetting),
130
131 Choice(ChoiceSetting),
135}
136
137#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
138#[serde(rename_all = "camelCase")]
139pub struct TextSetting {
140 #[builder(setter(strip_option), default)]
142 #[serde(skip_serializing_if = "Option::is_none")]
143 max_length: Option<u32>,
144
145 #[builder(setter(strip_option), default)]
151 #[serde(skip_serializing_if = "Option::is_none")]
152 is_password: Option<bool>,
153
154 #[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 #[builder(setter(strip_option), default)]
174 #[serde(skip_serializing_if = "Option::is_none")]
175 max_length: Option<u32>,
176
177 #[builder(setter(strip_option), default)]
183 #[serde(skip_serializing_if = "Option::is_none")]
184 is_password: Option<bool>,
185
186 #[builder(setter(strip_option), default)]
191 #[serde(skip_serializing_if = "Option::is_none")]
192 read_only: Option<bool>,
193
194 #[builder(setter(strip_option), default)]
196 #[serde(skip_serializing_if = "Option::is_none")]
197 min_value: Option<f64>,
198
199 #[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 #[builder(setter(strip_option), default)]
236 #[serde(skip_serializing_if = "Option::is_none")]
237 max_length: Option<u32>,
238
239 #[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 #[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 #[builder(setter(into, strip_option), default)]
285 #[serde(skip_serializing_if = "Option::is_none")]
286 pub(crate) title: Option<String>,
287
288 #[builder(setter(into))]
290 pub(crate) body: String,
291
292 #[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}