Skip to main content

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