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::{Metadata, MetadataSet};
149
150    use super::*;
151    use chrono::TimeZone;
152    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
153    use toml::value::Datetime;
154
155    #[test]
156    fn test_examples() {
157        use crate::test_examples;
158
159        test_examples(FromToml {})
160    }
161
162    #[test]
163    fn from_toml_creates_correct_date() {
164        let toml_date = toml::Value::Datetime(Datetime {
165            date: Option::from(toml::value::Date {
166                year: 1980,
167                month: 10,
168                day: 12,
169            }),
170            time: Option::from(toml::value::Time {
171                hour: 10,
172                minute: 12,
173                second: 44,
174                nanosecond: 0,
175            }),
176            offset: Option::from(toml::value::Offset::Custom { minutes: 120 }),
177        });
178
179        let span = Span::test_data();
180        let reference_date = Value::date(
181            chrono::FixedOffset::east_opt(60 * 120)
182                .unwrap()
183                .with_ymd_and_hms(1980, 10, 12, 10, 12, 44)
184                .unwrap(),
185            Span::test_data(),
186        );
187
188        let result = convert_toml_to_value(&toml_date, span);
189
190        //positive test (from toml returns a nushell date)
191        assert_eq!(result, reference_date);
192    }
193
194    #[test]
195    fn string_to_toml_value_passes() {
196        let input_string = String::from(
197            r#"
198            command.build = "go build"
199
200            [command.deploy]
201            script = "./deploy.sh"
202            "#,
203        );
204
205        let span = Span::test_data();
206
207        let result = convert_string_to_value(input_string, span);
208
209        assert!(result.is_ok());
210    }
211
212    #[test]
213    fn string_to_toml_value_fails() {
214        let input_string = String::from(
215            r#"
216            command.build =
217
218            [command.deploy]
219            script = "./deploy.sh"
220            "#,
221        );
222
223        let span = Span::test_data();
224
225        let result = convert_string_to_value(input_string, span);
226
227        assert!(result.is_err());
228    }
229
230    #[test]
231    fn convert_toml_datetime_to_value_date_time_offset() {
232        let toml_date = Datetime {
233            date: Option::from(toml::value::Date {
234                year: 2000,
235                month: 1,
236                day: 1,
237            }),
238            time: Option::from(toml::value::Time {
239                hour: 12,
240                minute: 12,
241                second: 12,
242                nanosecond: 0,
243            }),
244            offset: Option::from(toml::value::Offset::Custom { minutes: 120 }),
245        };
246
247        let span = Span::test_data();
248        let reference_date = Value::date(
249            chrono::FixedOffset::east_opt(60 * 120)
250                .unwrap()
251                .with_ymd_and_hms(2000, 1, 1, 12, 12, 12)
252                .unwrap(),
253            span,
254        );
255
256        let result = convert_toml_datetime_to_value(&toml_date, span);
257
258        assert_eq!(result, reference_date);
259    }
260
261    #[test]
262    fn convert_toml_datetime_to_value_date_time() {
263        let toml_date = Datetime {
264            date: Option::from(toml::value::Date {
265                year: 2000,
266                month: 1,
267                day: 1,
268            }),
269            time: Option::from(toml::value::Time {
270                hour: 12,
271                minute: 12,
272                second: 12,
273                nanosecond: 0,
274            }),
275            offset: None,
276        };
277
278        let span = Span::test_data();
279        let reference_date = Value::date(
280            chrono::FixedOffset::east_opt(0)
281                .unwrap()
282                .with_ymd_and_hms(2000, 1, 1, 12, 12, 12)
283                .unwrap(),
284            span,
285        );
286
287        let result = convert_toml_datetime_to_value(&toml_date, span);
288
289        assert_eq!(result, reference_date);
290    }
291
292    #[test]
293    fn convert_toml_datetime_to_value_date() {
294        let toml_date = Datetime {
295            date: Option::from(toml::value::Date {
296                year: 2000,
297                month: 1,
298                day: 1,
299            }),
300            time: None,
301            offset: None,
302        };
303
304        let span = Span::test_data();
305        let reference_date = Value::date(
306            chrono::FixedOffset::east_opt(0)
307                .unwrap()
308                .with_ymd_and_hms(2000, 1, 1, 0, 0, 0)
309                .unwrap(),
310            span,
311        );
312
313        let result = convert_toml_datetime_to_value(&toml_date, span);
314
315        assert_eq!(result, reference_date);
316    }
317
318    #[test]
319    fn convert_toml_datetime_to_value_only_time() {
320        let toml_date = Datetime {
321            date: None,
322            time: Option::from(toml::value::Time {
323                hour: 12,
324                minute: 12,
325                second: 12,
326                nanosecond: 0,
327            }),
328            offset: None,
329        };
330
331        let span = Span::test_data();
332        let reference_date = Value::string(toml_date.to_string(), span);
333
334        let result = convert_toml_datetime_to_value(&toml_date, span);
335
336        assert_eq!(result, reference_date);
337    }
338
339    #[test]
340    fn test_content_type_metadata() {
341        let mut engine_state = Box::new(EngineState::new());
342        let delta = {
343            let mut working_set = StateWorkingSet::new(&engine_state);
344
345            working_set.add_decl(Box::new(FromToml {}));
346            working_set.add_decl(Box::new(Metadata {}));
347            working_set.add_decl(Box::new(MetadataSet {}));
348
349            working_set.render()
350        };
351
352        engine_state
353            .merge_delta(delta)
354            .expect("Error merging delta");
355
356        let cmd = r#""[a]\nb = 1\nc = 1" | metadata set --content-type 'text/x-toml' --datasource-ls | from toml | metadata | $in"#;
357        let result = eval_pipeline_without_terminal_expression(
358            cmd,
359            std::env::temp_dir().as_ref(),
360            &mut engine_state,
361        );
362        assert_eq!(
363            Value::test_record(record!("source" => Value::test_string("ls"))),
364            result.expect("There should be a result")
365        )
366    }
367}