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