use crate::protocol::TouchPortalFromStr;
use derive_builder::Builder;
use serde::{Deserialize, Serialize};
use std::collections::BTreeSet;
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[builder(build_fn(validate = "Self::validate"))]
#[serde(rename_all = "camelCase")]
pub struct Setting {
#[builder(setter(into))]
pub(crate) name: String,
#[builder(setter(into))]
#[serde(rename = "default")]
pub(crate) initial: String,
#[serde(flatten)]
pub(crate) kind: SettingType,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) tooltip: Option<Tooltip>,
}
impl Setting {
pub fn builder() -> SettingBuilder {
SettingBuilder::default()
}
}
impl SettingBuilder {
fn validate(&self) -> Result<(), String> {
let initial = self.initial.as_ref().expect("initial is required");
let kind = self.kind.as_ref().expect("kind is required");
let max_length = match kind {
SettingType::Text(req) => req.max_length,
SettingType::Number(req) => req.max_length,
SettingType::Multiline(req) => req.max_length,
SettingType::File(_) => None,
SettingType::Folder(_) => None,
SettingType::Switch(_) => None,
SettingType::Choice(_) => None,
};
if let Some(max_length) = max_length
&& initial.len() > max_length as usize
{
return Err(format!(
"initial value '{initial}' is longer \
than allowed max length {max_length}"
));
}
if let SettingType::Choice(c) = kind
&& !c.choices.contains(initial.as_str())
{
return Err(format!(
"initial value '{initial}' is not among allowed choices"
));
}
if let SettingType::Number(n) = kind {
match f64::destringify(initial) {
Ok(v) if n.min_value.is_some_and(|min| v < min) => {
return Err(format!("initial value '{initial}' is below minimum value"));
}
Ok(v) if n.max_value.is_some_and(|max| v > max) => {
return Err(format!("initial value '{initial}' is above maximum value"));
}
Ok(_) => {}
Err(_) => return Err(format!("initial value '{initial}' is not numeric")),
}
}
if let SettingType::Switch(_) = kind {
match bool::destringify(initial) {
Ok(_) => {}
_ => {
return Err(format!(
"initial value '{initial}' is not switch-y (must be On or Off)"
));
}
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[non_exhaustive]
#[serde(rename_all = "lowercase")]
#[serde(tag = "type")]
pub enum SettingType {
Text(TextSetting),
Number(NumberSetting),
File(FileSetting),
Folder(FolderSetting),
Multiline(MultilineSetting),
Switch(SwitchSetting),
Choice(ChoiceSetting),
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TextSetting {
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_length: Option<u32>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
is_password: Option<bool>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
read_only: Option<bool>,
}
impl TextSetting {
pub fn builder() -> TextSettingBuilder {
TextSettingBuilder::default()
}
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct NumberSetting {
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_length: Option<u32>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
is_password: Option<bool>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
read_only: Option<bool>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
min_value: Option<f64>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_value: Option<f64>,
}
impl NumberSetting {
pub fn builder() -> NumberSettingBuilder {
NumberSettingBuilder::default()
}
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FileSetting {}
impl FileSetting {
pub fn builder() -> FileSettingBuilder {
FileSettingBuilder::default()
}
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct FolderSetting {}
impl FolderSetting {
pub fn builder() -> FolderSettingBuilder {
FolderSettingBuilder::default()
}
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MultilineSetting {
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
max_length: Option<u32>,
#[builder(setter(strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
read_only: Option<bool>,
}
impl MultilineSetting {
pub fn builder() -> MultilineSettingBuilder {
MultilineSettingBuilder::default()
}
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SwitchSetting {}
impl SwitchSetting {
pub fn builder() -> SwitchSettingBuilder {
SwitchSettingBuilder::default()
}
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChoiceSetting {
#[builder(setter(each(name = "choice", into)))]
pub(crate) choices: BTreeSet<String>,
}
impl ChoiceSetting {
pub fn builder() -> ChoiceSettingBuilder {
ChoiceSettingBuilder::default()
}
}
#[derive(Debug, Clone, Builder, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Tooltip {
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) title: Option<String>,
#[builder(setter(into))]
pub(crate) body: String,
#[builder(setter(into, strip_option), default)]
#[serde(skip_serializing_if = "Option::is_none")]
pub(crate) doc_url: Option<String>,
}
impl Tooltip {
pub fn builder() -> TooltipBuilder {
TooltipBuilder::default()
}
}
#[test]
fn serialize_example_setting() {
assert_eq!(
serde_json::to_value(
Setting::builder()
.name("Age")
.initial("23")
.kind(SettingType::Number(
NumberSetting::builder()
.max_length(20)
.is_password(false)
.min_value(0.0)
.max_value(120.0)
.read_only(false)
.build()
.unwrap()
))
.build()
.unwrap()
)
.unwrap(),
serde_json::json! {{
"name":"Age",
"default":"23",
"type":"number",
"maxLength":20,
"isPassword":false,
"minValue":0.,
"maxValue":120.,
"readOnly":false
}}
);
}
#[test]
fn serialize_example_setting_with_tooltip() {
assert_eq!(
serde_json::to_value(
Setting::builder()
.name("Age")
.initial("23")
.kind(SettingType::Number(
NumberSetting::builder()
.max_length(20)
.is_password(false)
.min_value(0.0)
.max_value(120.0)
.read_only(false)
.build()
.unwrap()
))
.tooltip(
Tooltip::builder()
.title("Toolstip")
.body(
"Learn more about how tooltips work in the Touch Portal API documentation."
)
.doc_url(
"https://www.touch-portal.com/api/v2/index.php?section=description_file_settings"
)
.build()
.unwrap()
)
.build()
.unwrap()
)
.unwrap(),
serde_json::json! {{
"name":"Age",
"default":"23",
"type":"number",
"maxLength":20,
"isPassword":false,
"minValue":0.,
"maxValue":120.,
"readOnly":false,
"tooltip":{
"title":"Toolstip",
"body":"Learn more about how tooltips work in the Touch Portal API documentation.",
"docUrl":"https://www.touch-portal.com/api/v2/index.php?section=description_file_settings"
}
}}
);
}