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
50fn 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 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
199fn to_toml_datetime(datetime: &DateTime<FixedOffset>) -> toml::value::Datetime {
202 let date = toml::value::Date {
203 year: datetime.year_ce().1 as u16,
207 month: datetime.month() as u8,
211 day: datetime.day() as u8,
212 };
213
214 let time = toml::value::Time {
215 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 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 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 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}