nu_command/formats/from/
toml.rs

1use nu_engine::command_prelude::*;
2use toml::value::{Datetime, Offset};
3
4#[derive(Clone)]
5pub struct FromToml;
6
7impl Command for FromToml {
8    fn name(&self) -> &str {
9        "from toml"
10    }
11
12    fn signature(&self) -> Signature {
13        Signature::build("from toml")
14            .input_output_types(vec![(Type::String, Type::record())])
15            .category(Category::Formats)
16    }
17
18    fn description(&self) -> &str {
19        "Parse text as .toml and create record."
20    }
21
22    fn run(
23        &self,
24        _engine_state: &EngineState,
25        _stack: &mut Stack,
26        call: &Call,
27        input: PipelineData,
28    ) -> Result<PipelineData, ShellError> {
29        let span = call.head;
30        let (mut string_input, span, metadata) = input.collect_string_strict(span)?;
31        string_input.push('\n');
32        Ok(convert_string_to_value(string_input, span)?
33            .into_pipeline_data_with_metadata(metadata.map(|md| md.with_content_type(None))))
34    }
35
36    fn examples(&self) -> Vec<Example> {
37        vec![
38            Example {
39                example: "'a = 1' | from toml",
40                description: "Converts toml formatted string to record",
41                result: Some(Value::test_record(record! {
42                    "a" => Value::test_int(1),
43                })),
44            },
45            Example {
46                example: "'a = 1
47b = [1, 2]' | from toml",
48                description: "Converts toml formatted string to record",
49                result: Some(Value::test_record(record! {
50                    "a" =>  Value::test_int(1),
51                    "b" =>  Value::test_list(vec![
52                        Value::test_int(1),
53                        Value::test_int(2)],),
54                })),
55            },
56        ]
57    }
58}
59
60fn convert_toml_datetime_to_value(dt: &Datetime, span: Span) -> Value {
61    match &dt.clone() {
62        toml::value::Datetime {
63            date: Some(_),
64            time: _,
65            offset: _,
66        } => (),
67        _ => return Value::string(dt.to_string(), span),
68    }
69
70    let date = match dt.date {
71        Some(date) => {
72            chrono::NaiveDate::from_ymd_opt(date.year.into(), date.month.into(), date.day.into())
73        }
74        None => Some(chrono::NaiveDate::default()),
75    };
76
77    let time = match dt.time {
78        Some(time) => chrono::NaiveTime::from_hms_nano_opt(
79            time.hour.into(),
80            time.minute.into(),
81            time.second.into(),
82            time.nanosecond,
83        ),
84        None => Some(chrono::NaiveTime::default()),
85    };
86
87    let tz = match dt.offset {
88        Some(offset) => match offset {
89            Offset::Z => chrono::FixedOffset::east_opt(0),
90            Offset::Custom { minutes: min } => chrono::FixedOffset::east_opt(min as i32 * 60),
91        },
92        None => chrono::FixedOffset::east_opt(0),
93    };
94
95    let datetime = match (date, time, tz) {
96        (Some(date), Some(time), Some(tz)) => chrono::NaiveDateTime::new(date, time)
97            .and_local_timezone(tz)
98            .earliest(),
99        _ => None,
100    };
101
102    match datetime {
103        Some(datetime) => Value::date(datetime, span),
104        None => Value::string(dt.to_string(), span),
105    }
106}
107
108fn convert_toml_to_value(value: &toml::Value, span: Span) -> Value {
109    match value {
110        toml::Value::Array(array) => {
111            let v: Vec<Value> = array
112                .iter()
113                .map(|x| convert_toml_to_value(x, span))
114                .collect();
115
116            Value::list(v, span)
117        }
118        toml::Value::Boolean(b) => Value::bool(*b, span),
119        toml::Value::Float(f) => Value::float(*f, span),
120        toml::Value::Integer(i) => Value::int(*i, span),
121        toml::Value::Table(k) => Value::record(
122            k.iter()
123                .map(|(k, v)| (k.clone(), convert_toml_to_value(v, span)))
124                .collect(),
125            span,
126        ),
127        toml::Value::String(s) => Value::string(s.clone(), span),
128        toml::Value::Datetime(dt) => convert_toml_datetime_to_value(dt, span),
129    }
130}
131
132pub fn convert_string_to_value(string_input: String, span: Span) -> Result<Value, ShellError> {
133    let result: Result<toml::Value, toml::de::Error> = toml::from_str(&string_input);
134    match result {
135        Ok(value) => Ok(convert_toml_to_value(&value, span)),
136
137        Err(err) => Err(ShellError::CantConvert {
138            to_type: "structured toml data".into(),
139            from_type: "string".into(),
140            span,
141            help: Some(err.to_string()),
142        }),
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use crate::Reject;
149    use crate::{Metadata, MetadataSet};
150
151    use super::*;
152    use chrono::TimeZone;
153    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
154    use toml::value::Datetime;
155
156    #[test]
157    fn test_examples() {
158        use crate::test_examples;
159
160        test_examples(FromToml {})
161    }
162
163    #[test]
164    fn from_toml_creates_correct_date() {
165        let toml_date = toml::Value::Datetime(Datetime {
166            date: Option::from(toml::value::Date {
167                year: 1980,
168                month: 10,
169                day: 12,
170            }),
171            time: Option::from(toml::value::Time {
172                hour: 10,
173                minute: 12,
174                second: 44,
175                nanosecond: 0,
176            }),
177            offset: Option::from(toml::value::Offset::Custom { minutes: 120 }),
178        });
179
180        let span = Span::test_data();
181        let reference_date = Value::date(
182            chrono::FixedOffset::east_opt(60 * 120)
183                .unwrap()
184                .with_ymd_and_hms(1980, 10, 12, 10, 12, 44)
185                .unwrap(),
186            Span::test_data(),
187        );
188
189        let result = convert_toml_to_value(&toml_date, span);
190
191        //positive test (from toml returns a nushell date)
192        assert_eq!(result, reference_date);
193    }
194
195    #[test]
196    fn string_to_toml_value_passes() {
197        let input_string = String::from(
198            r#"
199            command.build = "go build"
200
201            [command.deploy]
202            script = "./deploy.sh"
203            "#,
204        );
205
206        let span = Span::test_data();
207
208        let result = convert_string_to_value(input_string, span);
209
210        assert!(result.is_ok());
211    }
212
213    #[test]
214    fn string_to_toml_value_fails() {
215        let input_string = String::from(
216            r#"
217            command.build =
218
219            [command.deploy]
220            script = "./deploy.sh"
221            "#,
222        );
223
224        let span = Span::test_data();
225
226        let result = convert_string_to_value(input_string, span);
227
228        assert!(result.is_err());
229    }
230
231    #[test]
232    fn convert_toml_datetime_to_value_date_time_offset() {
233        let toml_date = Datetime {
234            date: Option::from(toml::value::Date {
235                year: 2000,
236                month: 1,
237                day: 1,
238            }),
239            time: Option::from(toml::value::Time {
240                hour: 12,
241                minute: 12,
242                second: 12,
243                nanosecond: 0,
244            }),
245            offset: Option::from(toml::value::Offset::Custom { minutes: 120 }),
246        };
247
248        let span = Span::test_data();
249        let reference_date = Value::date(
250            chrono::FixedOffset::east_opt(60 * 120)
251                .unwrap()
252                .with_ymd_and_hms(2000, 1, 1, 12, 12, 12)
253                .unwrap(),
254            span,
255        );
256
257        let result = convert_toml_datetime_to_value(&toml_date, span);
258
259        assert_eq!(result, reference_date);
260    }
261
262    #[test]
263    fn convert_toml_datetime_to_value_date_time() {
264        let toml_date = Datetime {
265            date: Option::from(toml::value::Date {
266                year: 2000,
267                month: 1,
268                day: 1,
269            }),
270            time: Option::from(toml::value::Time {
271                hour: 12,
272                minute: 12,
273                second: 12,
274                nanosecond: 0,
275            }),
276            offset: None,
277        };
278
279        let span = Span::test_data();
280        let reference_date = Value::date(
281            chrono::FixedOffset::east_opt(0)
282                .unwrap()
283                .with_ymd_and_hms(2000, 1, 1, 12, 12, 12)
284                .unwrap(),
285            span,
286        );
287
288        let result = convert_toml_datetime_to_value(&toml_date, span);
289
290        assert_eq!(result, reference_date);
291    }
292
293    #[test]
294    fn convert_toml_datetime_to_value_date() {
295        let toml_date = Datetime {
296            date: Option::from(toml::value::Date {
297                year: 2000,
298                month: 1,
299                day: 1,
300            }),
301            time: None,
302            offset: None,
303        };
304
305        let span = Span::test_data();
306        let reference_date = Value::date(
307            chrono::FixedOffset::east_opt(0)
308                .unwrap()
309                .with_ymd_and_hms(2000, 1, 1, 0, 0, 0)
310                .unwrap(),
311            span,
312        );
313
314        let result = convert_toml_datetime_to_value(&toml_date, span);
315
316        assert_eq!(result, reference_date);
317    }
318
319    #[test]
320    fn convert_toml_datetime_to_value_only_time() {
321        let toml_date = Datetime {
322            date: None,
323            time: Option::from(toml::value::Time {
324                hour: 12,
325                minute: 12,
326                second: 12,
327                nanosecond: 0,
328            }),
329            offset: None,
330        };
331
332        let span = Span::test_data();
333        let reference_date = Value::string(toml_date.to_string(), span);
334
335        let result = convert_toml_datetime_to_value(&toml_date, span);
336
337        assert_eq!(result, reference_date);
338    }
339
340    #[test]
341    fn test_content_type_metadata() {
342        let mut engine_state = Box::new(EngineState::new());
343        let delta = {
344            let mut working_set = StateWorkingSet::new(&engine_state);
345
346            working_set.add_decl(Box::new(FromToml {}));
347            working_set.add_decl(Box::new(Metadata {}));
348            working_set.add_decl(Box::new(MetadataSet {}));
349            working_set.add_decl(Box::new(Reject {}));
350
351            working_set.render()
352        };
353
354        engine_state
355            .merge_delta(delta)
356            .expect("Error merging delta");
357
358        let cmd = r#""[a]\nb = 1\nc = 1" | metadata set --content-type 'text/x-toml' --datasource-ls | from toml | metadata | reject span | $in"#;
359        let result = eval_pipeline_without_terminal_expression(
360            cmd,
361            std::env::temp_dir().as_ref(),
362            &mut engine_state,
363        );
364        assert_eq!(
365            Value::test_record(record!("source" => Value::test_string("ls"))),
366            result.expect("There should be a result")
367        )
368    }
369}