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