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