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