Skip to main content

nu_command/formats/to/
toml.rs

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