Skip to main content

tanzim_parse/
toml.rs

1//! TOML parser (`toml` feature).
2//!
3//! **Format:** `toml`
4//!
5//! # Example
6//!
7//! ```
8//! use tanzim_parse::{Deserialize, Toml};
9//!
10//! let value = Toml::new().parse("file", "config.toml", b"host = \"127.0.0.1\"\n").unwrap();
11//! assert_eq!(
12//!     value.value.as_map().unwrap().get("host").unwrap().value.as_string().unwrap(),
13//!     "127.0.0.1"
14//! );
15//! ```
16
17use crate::Deserialize;
18use crate::span::{char_count, is_single_line, line_column};
19use cfg_if::cfg_if;
20use tanzim_value::{Error, LocatedValue, Location, Map, Value};
21use toml_edit::{DocumentMut, Item, Table, Value as TomlValue};
22
23#[derive(Default, Debug, Clone, Copy)]
24pub struct Toml;
25
26impl Toml {
27    pub fn new() -> Self {
28        Self
29    }
30}
31
32impl Deserialize for Toml {
33    fn name(&self) -> &str {
34        "TOML"
35    }
36
37    fn supported_format_list(&self) -> Vec<String> {
38        vec!["toml".into()]
39    }
40
41    fn parse(&self, source: &str, resource: &str, bytes: &[u8]) -> Result<LocatedValue, Error> {
42        cfg_if! {
43            if #[cfg(feature = "tracing")] {
44                tracing::debug!(msg = "Parsing TOML configuration", source = source, resource = resource, bytes = bytes.len());
45            } else if #[cfg(feature = "logging")] {
46                log::debug!("msg=\"Parsing TOML configuration\" source={source} resource={resource} bytes={}", bytes.len());
47            }
48        }
49        let text = match std::str::from_utf8(bytes) {
50            Ok(value) => value,
51            Err(_) => {
52                return Err(Error::InvalidUtf8 {
53                    location: Location::at(source, resource, None, None, None),
54                });
55            }
56        };
57        let single_line = is_single_line(bytes);
58        let document = match text.parse::<DocumentMut>() {
59            Ok(value) => value,
60            Err(error) => {
61                let location = match error.span() {
62                    Some(span) => {
63                        let (line, column) = line_column(text, span.start);
64                        let length = char_count(text, span.start, span.end).max(1);
65                        Some(Location::at(
66                            source,
67                            resource,
68                            Some(line),
69                            Some(column),
70                            Some(length),
71                        ))
72                    }
73                    None => None,
74                };
75                return Err(Error::Parse {
76                    text: text.to_string(),
77                    location,
78                    message: error.message().to_string(),
79                });
80            }
81        };
82        let result = convert_table(source, resource, text, single_line, document.as_table(), 0);
83        if result.is_ok() {
84            cfg_if! {
85                if #[cfg(feature = "tracing")] {
86                    tracing::trace!(msg = "Parsed TOML configuration", source = source, resource = resource);
87                } else if #[cfg(feature = "logging")] {
88                    log::trace!("msg=\"Parsed TOML configuration\" source={source} resource={resource}");
89                }
90            }
91        }
92        result
93    }
94
95    fn is_format_supported(&self, bytes: &[u8]) -> Option<bool> {
96        match std::str::from_utf8(bytes) {
97            Ok(text) => Some(text.parse::<DocumentMut>().is_ok()),
98            Err(_) => Some(false),
99        }
100    }
101}
102
103fn convert_table(
104    source: &str,
105    resource: &str,
106    text: &str,
107    single_line: bool,
108    table: &Table,
109    fallback_offset: usize,
110) -> Result<LocatedValue, Error> {
111    let location = location_from_span(
112        source,
113        resource,
114        text,
115        single_line,
116        table.span(),
117        fallback_offset,
118    );
119    let mut map = Map::new();
120    for (key, item) in table {
121        let fallback_offset = span_start(item.span(), 0);
122        let location = location_from_span(
123            source,
124            resource,
125            text,
126            single_line,
127            item.span(),
128            fallback_offset,
129        );
130        let value = match item {
131            Item::Value(value) => {
132                convert_toml_value(source, resource, text, single_line, value, location)
133            }
134            Item::Table(table) => {
135                convert_table(source, resource, text, single_line, table, fallback_offset)
136            }
137            Item::ArrayOfTables(array) => {
138                let mut list = Vec::new();
139                for index in 0..array.len() {
140                    if let Some(table) = array.get(index) {
141                        list.push(convert_table(
142                            source,
143                            resource,
144                            text,
145                            single_line,
146                            table,
147                            span_start(table.span(), fallback_offset),
148                        )?);
149                    }
150                }
151                Ok(LocatedValue {
152                    value: Value::List(list),
153                    location,
154                })
155            }
156            Item::None => Err(Error::Parse {
157                text: text.to_string(),
158                location: Some(location),
159                message: "unexpected empty toml item".to_string(),
160            }),
161        }?;
162        map.insert(key.to_string(), value);
163    }
164    Ok(LocatedValue {
165        value: Value::Map(map),
166        location,
167    })
168}
169
170fn convert_toml_value(
171    source: &str,
172    resource: &str,
173    text: &str,
174    single_line: bool,
175    value: &TomlValue,
176    location: Location,
177) -> Result<LocatedValue, Error> {
178    match value {
179        TomlValue::String(value) => Ok(LocatedValue {
180            value: Value::String(value.value().to_string()),
181            location,
182        }),
183        TomlValue::Integer(value) => Ok(LocatedValue {
184            value: Value::Int(*value.value() as isize),
185            location,
186        }),
187        TomlValue::Float(value) => Ok(LocatedValue {
188            value: Value::Float(*value.value()),
189            location,
190        }),
191        TomlValue::Boolean(value) => Ok(LocatedValue {
192            value: Value::Bool(*value.value()),
193            location,
194        }),
195        TomlValue::Array(array) => {
196            let mut list = Vec::new();
197            let fallback_offset = span_start(array.span(), 0);
198            for index in 0..array.len() {
199                if let Some(value) = array.get(index) {
200                    let item_location = location_from_span(
201                        source,
202                        resource,
203                        text,
204                        single_line,
205                        value.span(),
206                        fallback_offset,
207                    );
208                    list.push(convert_toml_value(
209                        source,
210                        resource,
211                        text,
212                        single_line,
213                        value,
214                        item_location,
215                    )?);
216                }
217            }
218            Ok(LocatedValue {
219                value: Value::List(list),
220                location,
221            })
222        }
223        TomlValue::InlineTable(table) => {
224            let mut map = Map::new();
225            let fallback_offset = span_start(table.span(), 0);
226            for (key, value) in table {
227                let item_location = location_from_span(
228                    source,
229                    resource,
230                    text,
231                    single_line,
232                    value.span(),
233                    fallback_offset,
234                );
235                let converted =
236                    convert_toml_value(source, resource, text, single_line, value, item_location)?;
237                map.insert(key.to_string(), converted);
238            }
239            Ok(LocatedValue {
240                value: Value::Map(map),
241                location,
242            })
243        }
244        TomlValue::Datetime(_) => Err(Error::UnsupportedType {
245            text: text.to_string(),
246            location,
247            found: "datetime",
248        }),
249    }
250}
251
252fn span_start(span: Option<std::ops::Range<usize>>, fallback_offset: usize) -> usize {
253    match span {
254        Some(range) => range.start,
255        None => fallback_offset,
256    }
257}
258
259fn location_from_span(
260    source: &str,
261    resource: &str,
262    text: &str,
263    single_line: bool,
264    span: Option<std::ops::Range<usize>>,
265    fallback_offset: usize,
266) -> Location {
267    if single_line {
268        return Location::at(source, resource, None, None, None);
269    }
270    let mut length = 0usize;
271    if let Some(range) = &span {
272        length = char_count(text, range.start, range.end);
273    }
274    let offset = span_start(span, fallback_offset);
275    let (line, column) = line_column(text, offset);
276    Location::at(
277        source,
278        resource,
279        Some(line),
280        Some(column),
281        if length > 0 { Some(length) } else { None },
282    )
283}
284
285#[cfg(all(test, feature = "toml"))]
286mod tests {
287    use super::*;
288
289    #[test]
290    fn parses_toml_table() {
291        let parsed = Toml::new()
292            .parse("file", "config.toml", b"hello = \"world\"\n")
293            .unwrap();
294        assert_eq!(
295            parsed
296                .value
297                .as_map()
298                .unwrap()
299                .get("hello")
300                .unwrap()
301                .value
302                .as_string()
303                .unwrap(),
304            "world"
305        );
306    }
307
308    #[test]
309    fn syntax_error_has_location() {
310        let error = Toml::new()
311            .parse("file", "config.toml", b"hello = \n")
312            .unwrap_err();
313        if let Error::Parse { location, .. } = &error {
314            assert!(location.is_some());
315            assert_eq!(
316                location.as_ref().unwrap().line,
317                std::num::NonZeroU32::new(1)
318            );
319        } else {
320            panic!("expected parse error");
321        }
322        let message = format!("{error:#}");
323        assert!(message.contains('^'));
324    }
325}