nu_command/formats/to/
toml.rs

1use chrono::{DateTime, Datelike, FixedOffset, Timelike};
2use nu_engine::command_prelude::*;
3use nu_protocol::{PipelineMetadata, ast::PathMember};
4
5#[derive(Clone)]
6pub struct ToToml;
7
8impl Command for ToToml {
9    fn name(&self) -> &str {
10        "to toml"
11    }
12
13    fn signature(&self) -> Signature {
14        Signature::build("to toml")
15            .input_output_types(vec![(Type::record(), Type::String)])
16            .switch(
17                "serialize",
18                "serialize nushell types that cannot be deserialized",
19                Some('s'),
20            )
21            .category(Category::Formats)
22    }
23
24    fn description(&self) -> &str {
25        "Convert record into .toml text."
26    }
27
28    fn examples(&self) -> Vec<Example<'_>> {
29        vec![Example {
30            description: "Outputs an TOML string representing the contents of this record",
31            example: r#"{foo: 1 bar: 'qwe'} | to toml"#,
32            result: Some(Value::test_string("foo = 1\nbar = \"qwe\"\n")),
33        }]
34    }
35
36    fn run(
37        &self,
38        engine_state: &EngineState,
39        stack: &mut Stack,
40        call: &Call,
41        input: PipelineData,
42    ) -> Result<PipelineData, ShellError> {
43        let head = call.head;
44        let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
45
46        to_toml(engine_state, input, head, serialize_types)
47    }
48}
49
50// Helper method to recursively convert nu_protocol::Value -> toml::Value
51// This shouldn't be called at the top-level
52fn helper(
53    engine_state: &EngineState,
54    v: &Value,
55    serialize_types: bool,
56) -> Result<toml::Value, ShellError> {
57    Ok(match &v {
58        Value::Bool { val, .. } => toml::Value::Boolean(*val),
59        Value::Int { val, .. } => toml::Value::Integer(*val),
60        Value::Filesize { val, .. } => toml::Value::Integer(val.get()),
61        Value::Duration { val, .. } => toml::Value::String(val.to_string()),
62        Value::Date { val, .. } => toml::Value::Datetime(to_toml_datetime(val)),
63        Value::Range { .. } => toml::Value::String("<Range>".to_string()),
64        Value::Float { val, .. } => toml::Value::Float(*val),
65        Value::String { val, .. } | Value::Glob { val, .. } => toml::Value::String(val.clone()),
66        Value::Record { val, .. } => {
67            let mut m = toml::map::Map::new();
68            for (k, v) in &**val {
69                m.insert(k.clone(), helper(engine_state, v, serialize_types)?);
70            }
71            toml::Value::Table(m)
72        }
73        Value::List { vals, .. } => {
74            toml::Value::Array(toml_list(engine_state, vals, serialize_types)?)
75        }
76        Value::Closure { val, .. } => {
77            if serialize_types {
78                let block = engine_state.get_block(val.block_id);
79                if let Some(span) = block.span {
80                    let contents_bytes = engine_state.get_span_contents(span);
81                    let contents_string = String::from_utf8_lossy(contents_bytes);
82                    toml::Value::String(contents_string.to_string())
83                } else {
84                    toml::Value::String(format!(
85                        "unable to retrieve block contents for toml block_id {}",
86                        val.block_id.get()
87                    ))
88                }
89            } else {
90                toml::Value::String(format!("closure_{}", val.block_id.get()))
91            }
92        }
93        Value::Nothing { .. } => toml::Value::String("<Nothing>".to_string()),
94        Value::Error { error, .. } => return Err(*error.clone()),
95        Value::Binary { val, .. } => toml::Value::Array(
96            val.iter()
97                .map(|x| toml::Value::Integer(*x as i64))
98                .collect(),
99        ),
100        Value::CellPath { val, .. } => toml::Value::Array(
101            val.members
102                .iter()
103                .map(|x| match &x {
104                    PathMember::String { val, .. } => Ok(toml::Value::String(val.clone())),
105                    PathMember::Int { val, .. } => Ok(toml::Value::Integer(*val as i64)),
106                })
107                .collect::<Result<Vec<toml::Value>, ShellError>>()?,
108        ),
109        Value::Custom { .. } => toml::Value::String("<Custom Value>".to_string()),
110    })
111}
112
113fn toml_list(
114    engine_state: &EngineState,
115    input: &[Value],
116    serialize_types: bool,
117) -> Result<Vec<toml::Value>, ShellError> {
118    let mut out = vec![];
119
120    for value in input {
121        out.push(helper(engine_state, value, serialize_types)?);
122    }
123
124    Ok(out)
125}
126
127fn toml_into_pipeline_data(
128    toml_value: &toml::Value,
129    value_type: Type,
130    span: Span,
131    metadata: Option<PipelineMetadata>,
132) -> Result<PipelineData, ShellError> {
133    let new_md = Some(
134        metadata
135            .unwrap_or_default()
136            .with_content_type(Some("text/x-toml".into())),
137    );
138
139    match toml::to_string_pretty(&toml_value) {
140        Ok(serde_toml_string) => {
141            Ok(Value::string(serde_toml_string, span).into_pipeline_data_with_metadata(new_md))
142        }
143        _ => Ok(Value::error(
144            ShellError::CantConvert {
145                to_type: "TOML".into(),
146                from_type: value_type.to_string(),
147                span,
148                help: None,
149            },
150            span,
151        )
152        .into_pipeline_data_with_metadata(new_md)),
153    }
154}
155
156fn value_to_toml_value(
157    engine_state: &EngineState,
158    v: &Value,
159    head: Span,
160    serialize_types: bool,
161) -> Result<toml::Value, ShellError> {
162    match v {
163        Value::Record { .. } | Value::Closure { .. } => helper(engine_state, v, serialize_types),
164        // Propagate existing errors
165        Value::Error { error, .. } => Err(*error.clone()),
166        _ => Err(ShellError::UnsupportedInput {
167            msg: format!("{:?} is not valid top-level TOML", v.get_type()),
168            input: "value originates from here".into(),
169            msg_span: head,
170            input_span: v.span(),
171        }),
172    }
173}
174
175fn to_toml(
176    engine_state: &EngineState,
177    input: PipelineData,
178    span: Span,
179    serialize_types: bool,
180) -> Result<PipelineData, ShellError> {
181    let metadata = input.metadata();
182    let value = input.into_value(span)?;
183
184    let toml_value = value_to_toml_value(engine_state, &value, span, serialize_types)?;
185    match toml_value {
186        toml::Value::Array(ref vec) => match vec[..] {
187            [toml::Value::Table(_)] => toml_into_pipeline_data(
188                vec.iter().next().expect("this should never trigger"),
189                value.get_type(),
190                span,
191                metadata,
192            ),
193            _ => toml_into_pipeline_data(&toml_value, value.get_type(), span, metadata),
194        },
195        _ => toml_into_pipeline_data(&toml_value, value.get_type(), span, metadata),
196    }
197}
198
199/// Convert chrono datetime into a toml::Value datetime.  The latter uses its
200/// own ad-hoc datetime types, which makes this somewhat convoluted.
201fn to_toml_datetime(datetime: &DateTime<FixedOffset>) -> toml::value::Datetime {
202    let date = toml::value::Date {
203        // TODO: figure out what to do with BC dates, because the toml
204        // crate doesn't support them.  Same for large years, which
205        // don't fit in u16.
206        year: datetime.year_ce().1 as u16,
207        // Panic: this is safe, because chrono guarantees that the month
208        // value will be between 1 and 12 and the day will be between 1
209        // and 31
210        month: datetime.month() as u8,
211        day: datetime.day() as u8,
212    };
213
214    let time = toml::value::Time {
215        // Panic: same as before, chorono guarantees that all of the following 3
216        // methods return values less than 65'000
217        hour: datetime.hour() as u8,
218        minute: datetime.minute() as u8,
219        second: datetime.second() as u8,
220        nanosecond: datetime.nanosecond(),
221    };
222
223    let offset = toml::value::Offset::Custom {
224        // Panic: minute timezone offset fits into i16 (that's more than
225        // 1000 hours)
226        minutes: (-datetime.timezone().utc_minus_local() / 60) as i16,
227    };
228
229    toml::value::Datetime {
230        date: Some(date),
231        time: Some(time),
232        offset: Some(offset),
233    }
234}
235
236#[cfg(test)]
237mod tests {
238    use super::*;
239    use chrono::TimeZone;
240
241    #[test]
242    fn test_examples() {
243        use crate::test_examples;
244
245        test_examples(ToToml {})
246    }
247
248    #[test]
249    fn to_toml_creates_correct_date() {
250        let engine_state = EngineState::new();
251        let serialize_types = false;
252
253        let test_date = Value::date(
254            chrono::FixedOffset::east_opt(60 * 120)
255                .unwrap()
256                .with_ymd_and_hms(1980, 10, 12, 10, 12, 44)
257                .unwrap(),
258            Span::test_data(),
259        );
260
261        let reference_date = toml::Value::Datetime(toml::value::Datetime {
262            date: Some(toml::value::Date {
263                year: 1980,
264                month: 10,
265                day: 12,
266            }),
267            time: Some(toml::value::Time {
268                hour: 10,
269                minute: 12,
270                second: 44,
271                nanosecond: 0,
272            }),
273            offset: Some(toml::value::Offset::Custom { minutes: 120 }),
274        });
275
276        let result = helper(&engine_state, &test_date, serialize_types);
277
278        assert!(result.is_ok_and(|res| res == reference_date));
279    }
280
281    #[test]
282    fn test_value_to_toml_value() {
283        //
284        // Positive Tests
285        //
286
287        let engine_state = EngineState::new();
288        let serialize_types = false;
289
290        let mut m = indexmap::IndexMap::new();
291        m.insert("rust".to_owned(), Value::test_string("editor"));
292        m.insert("is".to_owned(), Value::nothing(Span::test_data()));
293        m.insert(
294            "features".to_owned(),
295            Value::list(
296                vec![Value::test_string("hello"), Value::test_string("array")],
297                Span::test_data(),
298            ),
299        );
300        let tv = value_to_toml_value(
301            &engine_state,
302            &Value::record(m.into_iter().collect(), Span::test_data()),
303            Span::test_data(),
304            serialize_types,
305        )
306        .expect("Expected Ok from valid TOML dictionary");
307        assert_eq!(
308            tv.get("features"),
309            Some(&toml::Value::Array(vec![
310                toml::Value::String("hello".to_owned()),
311                toml::Value::String("array".to_owned())
312            ]))
313        );
314        //
315        // Negative Tests
316        //
317        value_to_toml_value(
318            &engine_state,
319            &Value::test_string("not_valid"),
320            Span::test_data(),
321            serialize_types,
322        )
323        .expect_err("Expected non-valid toml (String) to cause error!");
324        value_to_toml_value(
325            &engine_state,
326            &Value::list(vec![Value::test_string("1")], Span::test_data()),
327            Span::test_data(),
328            serialize_types,
329        )
330        .expect_err("Expected non-valid toml (Table) to cause error!");
331    }
332}