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
51pub(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 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
222fn to_toml_datetime(datetime: &DateTime<FixedOffset>) -> toml::value::Datetime {
225 let date = toml::value::Date {
226 year: datetime.year_ce().1 as u16,
230 month: datetime.month() as u8,
234 day: datetime.day() as u8,
235 };
236
237 let time = toml::value::Time {
238 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 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 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 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}