1use std::fmt::Write as _;
4
5#[derive(Debug, Clone, PartialEq)]
7pub enum SettingValue {
8 Bool(bool),
10 Integer(i64),
12 Float(f64),
14 Text(String),
16 List(Vec<SettingValue>),
18}
19
20impl SettingValue {
21 #[must_use]
23 pub fn type_name(&self) -> &'static str {
24 match self {
25 Self::Bool(_) => "boolean",
26 Self::Integer(_) => "integer",
27 Self::Float(_) => "float",
28 Self::Text(_) => "string",
29 Self::List(_) => "array",
30 }
31 }
32
33 pub(crate) fn literal(&self) -> String {
35 let mut out = String::new();
36 self.write(&mut out);
37 out
38 }
39
40 pub(crate) fn write(&self, out: &mut String) {
42 match self {
45 Self::Bool(value) => out.push_str(if *value { "true" } else { "false" }),
46 Self::Integer(value) => {
47 let _ = write!(out, "{value}");
48 }
49 Self::Float(value) => out.push_str(&float(*value)),
50 Self::Text(text) => quote(text, out),
51 Self::List(items) => {
52 out.push('[');
53 for (index, item) in items.iter().enumerate() {
54 if index > 0 {
55 out.push_str(", ");
56 }
57 item.write(out);
58 }
59 out.push(']');
60 }
61 }
62 }
63}
64
65fn float(value: f64) -> String {
66 if value.is_nan() {
67 "nan".to_owned()
68 } else if value.is_infinite() {
69 if value > 0.0 { "inf".to_owned() } else { "-inf".to_owned() }
70 } else {
71 format!("{value:?}")
73 }
74}
75
76pub(crate) fn quote(text: &str, out: &mut String) {
78 out.push('"');
79 for c in text.chars() {
80 match c {
81 '"' => out.push_str("\\\""),
82 '\\' => out.push_str("\\\\"),
83 '\n' => out.push_str("\\n"),
84 '\r' => out.push_str("\\r"),
85 '\t' => out.push_str("\\t"),
86 c if c.is_control() => {
88 let _ = write!(out, "\\u{:04X}", u32::from(c));
89 }
90 c => out.push(c),
91 }
92 }
93 out.push('"');
94}
95
96pub(crate) fn key(segment: &str, out: &mut String) {
98 let bare = !segment.is_empty() && segment.chars().all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_');
99 if bare {
100 out.push_str(segment);
101 } else {
102 quote(segment, out);
103 }
104}
105
106pub trait Setting: Sized {
111 fn to_setting(self) -> SettingValue;
113
114 fn from_setting(value: &SettingValue) -> Option<Self>;
116}
117
118impl Setting for bool {
119 fn to_setting(self) -> SettingValue {
120 SettingValue::Bool(self)
121 }
122
123 fn from_setting(value: &SettingValue) -> Option<Self> {
124 match value {
125 SettingValue::Bool(value) => Some(*value),
126 _ => None,
127 }
128 }
129}
130
131impl Setting for String {
132 fn to_setting(self) -> SettingValue {
133 SettingValue::Text(self)
134 }
135
136 fn from_setting(value: &SettingValue) -> Option<Self> {
137 match value {
138 SettingValue::Text(text) => Some(text.clone()),
139 _ => None,
140 }
141 }
142}
143
144impl Setting for f64 {
145 fn to_setting(self) -> SettingValue {
146 SettingValue::Float(self)
147 }
148
149 fn from_setting(value: &SettingValue) -> Option<Self> {
150 match value {
151 SettingValue::Float(value) => Some(*value),
152 SettingValue::Integer(value) => i32::try_from(*value).ok().map(f64::from),
154 _ => None,
155 }
156 }
157}
158
159impl Setting for f32 {
160 fn to_setting(self) -> SettingValue {
161 SettingValue::Float(f64::from(self))
162 }
163
164 fn from_setting(value: &SettingValue) -> Option<Self> {
165 f64::from_setting(value).map(|value| value as f32)
167 }
168}
169
170macro_rules! integer_setting {
171 ($($ty:ty),*) => {$(
172 impl Setting for $ty {
173 fn to_setting(self) -> SettingValue {
174 SettingValue::Integer(i64::try_from(self).unwrap_or(i64::MAX))
175 }
176
177 fn from_setting(value: &SettingValue) -> Option<Self> {
178 match value {
179 SettingValue::Integer(value) => <$ty>::try_from(*value).ok(),
180 _ => None,
181 }
182 }
183 }
184 )*};
185}
186
187integer_setting!(i8, i16, i32, i64, u8, u16, u32, u64, usize);
188
189impl Setting for Vec<String> {
190 fn to_setting(self) -> SettingValue {
191 SettingValue::List(self.into_iter().map(SettingValue::Text).collect())
192 }
193
194 fn from_setting(value: &SettingValue) -> Option<Self> {
195 match value {
196 SettingValue::List(items) => items.iter().map(String::from_setting).collect(),
197 _ => None,
198 }
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn writes_toml_literals() {
208 assert_eq!(SettingValue::Text("a \"b\"\n\\".into()).literal(), r#""a \"b\"\n\\""#);
209 assert_eq!(SettingValue::Float(1.0).literal(), "1.0");
210 assert_eq!(SettingValue::Float(f64::NAN).literal(), "nan");
211 assert_eq!(vec!["x".to_owned(), "y".to_owned()].to_setting().literal(), r#"["x", "y"]"#);
212 let mut out = String::new();
213 key("tab width", &mut out);
214 key("tab-width", &mut out);
215 assert_eq!(out, "\"tab width\"tab-width");
216 }
217
218 #[test]
219 fn typed_reads_check_types_and_ranges() {
220 assert_eq!(u8::from_setting(&SettingValue::Integer(300)), None);
221 assert_eq!(u16::from_setting(&SettingValue::Integer(300)), Some(300));
222 assert_eq!(f64::from_setting(&SettingValue::Integer(2)), Some(2.0));
223 assert_eq!(bool::from_setting(&SettingValue::Text("true".into())), None);
224 }
225}