1use toml_edit::{Array, Datetime, InlineTable, Item, Table, Value};
9
10#[derive(Clone, Copy, PartialEq, Eq, Debug)]
12pub enum Kind {
13 Text,
15 Integer,
17 Float,
19 Boolean,
21 OffsetDateTime,
23 LocalDateTime,
25 LocalDate,
27 LocalTime,
29 Array,
31 InlineTable,
33 Table,
35}
36
37impl Kind {
38 pub const SCALARS: [Self; 8] = [
40 Self::Text,
41 Self::Integer,
42 Self::Float,
43 Self::Boolean,
44 Self::OffsetDateTime,
45 Self::LocalDateTime,
46 Self::LocalDate,
47 Self::LocalTime,
48 ];
49
50 pub const VALUES: [Self; 10] = [
52 Self::Text,
53 Self::Integer,
54 Self::Float,
55 Self::Boolean,
56 Self::OffsetDateTime,
57 Self::LocalDateTime,
58 Self::LocalDate,
59 Self::LocalTime,
60 Self::Array,
61 Self::InlineTable,
62 ];
63
64 pub const ALL: [Self; 11] = [
66 Self::Text,
67 Self::Integer,
68 Self::Float,
69 Self::Boolean,
70 Self::OffsetDateTime,
71 Self::LocalDateTime,
72 Self::LocalDate,
73 Self::LocalTime,
74 Self::Array,
75 Self::InlineTable,
76 Self::Table,
77 ];
78
79 #[must_use]
81 pub fn label(self) -> &'static str {
82 match self {
83 Self::Text => "text",
84 Self::Integer => "integer",
85 Self::Float => "float",
86 Self::Boolean => "boolean",
87 Self::OffsetDateTime => "offset date-time",
88 Self::LocalDateTime => "local date-time",
89 Self::LocalDate => "local date",
90 Self::LocalTime => "local time",
91 Self::Array => "array",
92 Self::InlineTable => "inline table",
93 Self::Table => "table",
94 }
95 }
96
97 #[must_use]
99 pub fn of_value(v: &Value) -> Self {
100 match v {
101 Value::String(_) => Self::Text,
102 Value::Integer(_) => Self::Integer,
103 Value::Float(_) => Self::Float,
104 Value::Boolean(_) => Self::Boolean,
105 Value::Datetime(d) => Self::of_datetime(d.value()),
106 Value::Array(_) => Self::Array,
107 Value::InlineTable(_) => Self::InlineTable,
108 }
109 }
110
111 #[must_use]
114 pub fn of_item(item: &Item) -> Option<Self> {
115 match item {
116 Item::Value(v) => Some(Self::of_value(v)),
117 Item::Table(_) => Some(Self::Table),
118 Item::None | Item::ArrayOfTables(_) => None,
119 }
120 }
121
122 fn of_datetime(d: &Datetime) -> Self {
123 match (d.date.is_some(), d.time.is_some(), d.offset.is_some()) {
124 (true, true, true) => Self::OffsetDateTime,
125 (true, true, false) => Self::LocalDateTime,
126 (true, false, _) => Self::LocalDate,
127 (false, _, _) => Self::LocalTime,
128 }
129 }
130
131 #[must_use]
133 pub fn as_value(self) -> Option<Value> {
134 match self.item() {
135 Item::Value(v) => Some(v),
136 _ => None,
137 }
138 }
139
140 #[must_use]
148 pub fn item(self) -> Item {
149 let datetime = |text: &str| {
150 Item::Value(Value::from(
151 text.parse::<Datetime>().expect("a fixed datetime literal"),
152 ))
153 };
154 match self {
155 Self::Text => Item::Value(Value::from("")),
156 Self::Integer => Item::Value(Value::from(0_i64)),
157 Self::Float => Item::Value(Value::from(0.0_f64)),
158 Self::Boolean => Item::Value(Value::from(false)),
159 Self::OffsetDateTime => datetime("1970-01-01T00:00:00Z"),
160 Self::LocalDateTime => datetime("1970-01-01T00:00:00"),
161 Self::LocalDate => datetime("1970-01-01"),
162 Self::LocalTime => datetime("00:00:00"),
163 Self::Array => Item::Value(Value::Array(Array::new())),
164 Self::InlineTable => Item::Value(Value::InlineTable(InlineTable::new())),
165 Self::Table => Item::Table(Table::new()),
166 }
167 }
168}
169
170#[must_use]
182pub fn convert(v: &Value, to: Kind) -> Option<Value> {
183 if Kind::of_value(v) == to {
184 return Some(v.clone());
185 }
186 match to {
187 Kind::Text => Some(Value::from(scalar_text(v)?)),
188 Kind::Integer => match v {
189 Value::String(s) => s.value().trim().parse::<i64>().ok().map(Value::from),
190 Value::Float(f) if f.value().fract() == 0.0 && f.value().is_finite() => {
191 let x = *f.value();
195 #[allow(clippy::cast_possible_truncation)]
196 (x.abs() < 9.2e18).then(|| Value::from(x as i64))
197 }
198 Value::Array(a) => single(a).and_then(|e| convert(e, to)),
199 _ => None,
200 },
201 Kind::Float => match v {
202 Value::String(s) => s.value().trim().parse::<f64>().ok().map(Value::from),
203 #[allow(clippy::cast_precision_loss)]
206 Value::Integer(i) => Some(Value::from(*i.value() as f64)),
207 Value::Array(a) => single(a).and_then(|e| convert(e, to)),
208 _ => None,
209 },
210 Kind::Boolean => match v {
211 Value::String(s) => match s.value().trim() {
212 "true" => Some(Value::from(true)),
213 "false" => Some(Value::from(false)),
214 _ => None,
215 },
216 Value::Array(a) => single(a).and_then(|e| convert(e, to)),
217 _ => None,
218 },
219 Kind::OffsetDateTime | Kind::LocalDateTime | Kind::LocalDate | Kind::LocalTime => {
220 let d = match v {
221 Value::String(s) => s.value().trim().parse::<Datetime>().ok()?,
222 Value::Datetime(d) => *d.value(),
223 Value::Array(a) => return single(a).and_then(|e| convert(e, to)),
224 _ => return None,
225 };
226 Some(Value::from(reshape(d, to)))
227 }
228 Kind::Array => {
229 let mut element = v.clone();
232 *element.decor_mut() = toml_edit::Decor::default();
233 let mut a = Array::new();
234 a.push_formatted(element);
235 Some(Value::Array(a))
236 }
237 Kind::InlineTable | Kind::Table => None,
238 }
239}
240
241fn scalar_text(v: &Value) -> Option<String> {
244 match v {
245 Value::String(s) => Some(s.value().clone()),
246 Value::Integer(i) => Some(i.value().to_string()),
247 Value::Float(f) => Some(f.value().to_string()),
248 Value::Boolean(b) => Some(b.value().to_string()),
249 Value::Datetime(d) => Some(d.value().to_string()),
250 Value::Array(a) => single(a).and_then(scalar_text),
251 Value::InlineTable(_) => None,
252 }
253}
254
255fn single(a: &Array) -> Option<&Value> {
257 (a.len() == 1).then(|| a.get(0)).flatten()
258}
259
260fn reshape(d: Datetime, to: Kind) -> Datetime {
264 let epoch_date = "1970-01-01".parse::<Datetime>().expect("a date").date;
265 let midnight = "00:00:00".parse::<Datetime>().expect("a time").time;
266 let utc = "1970-01-01T00:00:00Z"
267 .parse::<Datetime>()
268 .expect("an offset")
269 .offset;
270 let date = d.date.or(epoch_date);
271 let time = d.time.or(midnight);
272 match to {
273 Kind::OffsetDateTime => Datetime {
274 date,
275 time,
276 offset: d.offset.or(utc),
277 },
278 Kind::LocalDateTime => Datetime {
279 date,
280 time,
281 offset: None,
282 },
283 Kind::LocalDate => Datetime {
284 date,
285 time: None,
286 offset: None,
287 },
288 _ => Datetime {
289 date: None,
290 time,
291 offset: None,
292 },
293 }
294}
295
296#[cfg(test)]
297mod tests {
298 use super::{convert, Kind};
299 use toml_edit::{DocumentMut, Value};
300
301 fn value(text: &str) -> Value {
302 let doc: DocumentMut = format!("v = {text}\n").parse().expect("valid TOML");
303 doc["v"].as_value().expect("a value").clone()
304 }
305
306 fn text_of(v: &Value) -> String {
307 v.to_string().trim().to_owned()
308 }
309
310 #[test]
313 fn the_four_datetime_shapes_are_told_apart() {
314 assert_eq!(
315 Kind::of_value(&value("1979-05-27T07:32:00Z")),
316 Kind::OffsetDateTime
317 );
318 assert_eq!(
319 Kind::of_value(&value("1979-05-27T07:32:00")),
320 Kind::LocalDateTime
321 );
322 assert_eq!(Kind::of_value(&value("1979-05-27")), Kind::LocalDate);
323 assert_eq!(Kind::of_value(&value("07:32:00")), Kind::LocalTime);
324 }
325
326 #[test]
330 fn every_kind_starts_as_itself() {
331 for kind in Kind::ALL {
332 let item = kind.item();
333 assert_eq!(Kind::of_item(&item), Some(kind), "{kind:?}");
334 let mut doc = DocumentMut::new();
335 doc.as_table_mut().insert("k", item);
336 doc.to_string().parse::<DocumentMut>().expect("valid TOML");
337 }
338 }
339
340 #[test]
342 fn conversions_that_read_as_the_target_succeed() {
343 let cases: &[(&str, Kind, &str)] = &[
344 ("44", Kind::Text, "\"44\""),
345 ("1.5", Kind::Text, "\"1.5\""),
346 ("true", Kind::Text, "\"true\""),
347 ("1979-05-27", Kind::Text, "\"1979-05-27\""),
348 ("\"44\"", Kind::Integer, "44"),
349 ("\" 44 \"", Kind::Integer, "44"),
350 ("2.0", Kind::Integer, "2"),
351 ("\"1.5\"", Kind::Float, "1.5"),
352 ("2", Kind::Float, "2.0"),
353 ("\"true\"", Kind::Boolean, "true"),
354 ("\"1979-05-27\"", Kind::LocalDate, "1979-05-27"),
355 (
356 "1979-05-27T07:32:00Z",
357 Kind::LocalDateTime,
358 "1979-05-27T07:32:00",
359 ),
360 ("1979-05-27T07:32:00Z", Kind::LocalDate, "1979-05-27"),
361 ("1979-05-27T07:32:00Z", Kind::LocalTime, "07:32:00"),
362 (
363 "1979-05-27T07:32:00",
364 Kind::OffsetDateTime,
365 "1979-05-27T07:32:00Z",
366 ),
367 ("1979-05-27", Kind::LocalDateTime, "1979-05-27T00:00:00"),
368 ("07:32:00", Kind::LocalDateTime, "1970-01-01T07:32:00"),
369 ("44", Kind::Array, "[44]"),
370 ("[44]", Kind::Integer, "44"),
371 ("[\"x\"]", Kind::Text, "\"x\""),
372 ("44", Kind::Integer, "44"),
373 ];
374 for (from, to, expected) in cases {
375 let got = convert(&value(from), *to)
376 .unwrap_or_else(|| panic!("{from} -> {to:?} was refused"));
377 assert_eq!(text_of(&got), *expected, "{from} -> {to:?}");
378 assert_eq!(
379 Kind::of_value(&got),
380 *to,
381 "{from} -> {to:?} is not a {to:?}"
382 );
383 }
384 }
385
386 #[test]
388 fn conversions_that_would_guess_are_refused() {
389 let cases: &[(&str, Kind)] = &[
390 ("true", Kind::Integer),
391 ("1", Kind::Boolean),
392 ("1.5", Kind::Integer),
393 ("\"forty-four\"", Kind::Integer),
394 ("\"yes\"", Kind::Boolean),
395 ("\"tomorrow\"", Kind::LocalDate),
396 ("1979-05-27", Kind::Integer),
397 ("[1, 2]", Kind::Integer),
398 ("44", Kind::InlineTable),
399 ("{ a = 1 }", Kind::Text),
400 ("inf", Kind::Integer),
401 ("1e300", Kind::Integer),
402 ];
403 for (from, to) in cases {
404 assert!(
405 convert(&value(from), *to).is_none(),
406 "{from} -> {to:?} was allowed"
407 );
408 }
409 }
410}