1use 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}