1use indexmap::IndexMap;
2use itertools::Itertools;
3use nu_engine::command_prelude::*;
4use serde::de::Deserialize;
5
6#[derive(Clone)]
7pub struct FromYaml;
8
9impl Command for FromYaml {
10 fn name(&self) -> &str {
11 "from yaml"
12 }
13
14 fn signature(&self) -> Signature {
15 Signature::build("from yaml")
16 .input_output_types(vec![(Type::String, Type::Any)])
17 .category(Category::Formats)
18 }
19
20 fn description(&self) -> &str {
21 "Parse text as .yaml/.yml and create table."
22 }
23
24 fn examples(&self) -> Vec<Example> {
25 get_examples()
26 }
27
28 fn run(
29 &self,
30 _engine_state: &EngineState,
31 _stack: &mut Stack,
32 call: &Call,
33 input: PipelineData,
34 ) -> Result<PipelineData, ShellError> {
35 let head = call.head;
36 from_yaml(input, head)
37 }
38}
39
40#[derive(Clone)]
41pub struct FromYml;
42
43impl Command for FromYml {
44 fn name(&self) -> &str {
45 "from yml"
46 }
47
48 fn signature(&self) -> Signature {
49 Signature::build("from yml")
50 .input_output_types(vec![(Type::String, Type::Any)])
51 .category(Category::Formats)
52 }
53
54 fn description(&self) -> &str {
55 "Parse text as .yaml/.yml and create table."
56 }
57
58 fn run(
59 &self,
60 _engine_state: &EngineState,
61 _stack: &mut Stack,
62 call: &Call,
63 input: PipelineData,
64 ) -> Result<PipelineData, ShellError> {
65 let head = call.head;
66 from_yaml(input, head)
67 }
68
69 fn examples(&self) -> Vec<Example> {
70 get_examples()
71 }
72}
73
74fn convert_yaml_value_to_nu_value(
75 v: &serde_yaml::Value,
76 span: Span,
77 val_span: Span,
78) -> Result<Value, ShellError> {
79 let err_not_compatible_number = ShellError::UnsupportedInput {
80 msg: "Expected a nu-compatible number in YAML input".to_string(),
81 input: "value originates from here".into(),
82 msg_span: span,
83 input_span: val_span,
84 };
85 Ok(match v {
86 serde_yaml::Value::Bool(b) => Value::bool(*b, span),
87 serde_yaml::Value::Number(n) if n.is_i64() => {
88 Value::int(n.as_i64().ok_or(err_not_compatible_number)?, span)
89 }
90 serde_yaml::Value::Number(n) if n.is_f64() => {
91 Value::float(n.as_f64().ok_or(err_not_compatible_number)?, span)
92 }
93 serde_yaml::Value::String(s) => Value::string(s.to_string(), span),
94 serde_yaml::Value::Sequence(a) => {
95 let result: Result<Vec<Value>, ShellError> = a
96 .iter()
97 .map(|x| convert_yaml_value_to_nu_value(x, span, val_span))
98 .collect();
99 Value::list(result?, span)
100 }
101 serde_yaml::Value::Mapping(t) => {
102 let mut collected = IndexMap::new();
104
105 for (k, v) in t {
106 let err_unexpected_map = ShellError::UnsupportedInput {
108 msg: format!("Unexpected YAML:\nKey: {k:?}\nValue: {v:?}"),
109 input: "value originates from here".into(),
110 msg_span: span,
111 input_span: val_span,
112 };
113 match (k, v) {
114 (serde_yaml::Value::Number(k), _) => {
115 collected.insert(
116 k.to_string(),
117 convert_yaml_value_to_nu_value(v, span, val_span)?,
118 );
119 }
120 (serde_yaml::Value::Bool(k), _) => {
121 collected.insert(
122 k.to_string(),
123 convert_yaml_value_to_nu_value(v, span, val_span)?,
124 );
125 }
126 (serde_yaml::Value::String(k), _) => {
127 collected.insert(
128 k.clone(),
129 convert_yaml_value_to_nu_value(v, span, val_span)?,
130 );
131 }
132 (serde_yaml::Value::Mapping(m), serde_yaml::Value::Null) => {
138 return m
139 .iter()
140 .take(1)
141 .collect_vec()
142 .first()
143 .and_then(|e| match e {
144 (serde_yaml::Value::String(s), serde_yaml::Value::Null) => {
145 Some(Value::string("{{ ".to_owned() + s.as_str() + " }}", span))
146 }
147 _ => None,
148 })
149 .ok_or(err_unexpected_map);
150 }
151 (_, _) => {
152 return Err(err_unexpected_map);
153 }
154 }
155 }
156
157 Value::record(collected.into_iter().collect(), span)
158 }
159 serde_yaml::Value::Tagged(t) => {
160 let tag = &t.tag;
161
162 match &t.value {
163 serde_yaml::Value::String(s) => {
164 let val = format!("{tag} {s}").trim().to_string();
165 Value::string(val, span)
166 }
167 serde_yaml::Value::Number(n) => {
168 let val = format!("{tag} {n}").trim().to_string();
169 Value::string(val, span)
170 }
171 serde_yaml::Value::Bool(b) => {
172 let val = format!("{tag} {b}").trim().to_string();
173 Value::string(val, span)
174 }
175 serde_yaml::Value::Null => {
176 let val = format!("{tag}").trim().to_string();
177 Value::string(val, span)
178 }
179 v => convert_yaml_value_to_nu_value(v, span, val_span)?,
180 }
181 }
182 serde_yaml::Value::Null => Value::nothing(span),
183 x => unimplemented!("Unsupported YAML case: {:?}", x),
184 })
185}
186
187pub fn from_yaml_string_to_value(s: &str, span: Span, val_span: Span) -> Result<Value, ShellError> {
188 let mut documents = vec![];
189
190 for document in serde_yaml::Deserializer::from_str(s) {
191 let v: serde_yaml::Value =
192 serde_yaml::Value::deserialize(document).map_err(|x| ShellError::UnsupportedInput {
193 msg: format!("Could not load YAML: {x}"),
194 input: "value originates from here".into(),
195 msg_span: span,
196 input_span: val_span,
197 })?;
198 documents.push(convert_yaml_value_to_nu_value(&v, span, val_span)?);
199 }
200
201 match documents.len() {
202 0 => Ok(Value::nothing(span)),
203 1 => Ok(documents.remove(0)),
204 _ => Ok(Value::list(documents, span)),
205 }
206}
207
208pub fn get_examples() -> Vec<Example<'static>> {
209 vec![
210 Example {
211 example: "'a: 1' | from yaml",
212 description: "Converts yaml formatted string to table",
213 result: Some(Value::test_record(record! {
214 "a" => Value::test_int(1),
215 })),
216 },
217 Example {
218 example: "'[ a: 1, b: [1, 2] ]' | from yaml",
219 description: "Converts yaml formatted string to table",
220 result: Some(Value::test_list(vec![
221 Value::test_record(record! {
222 "a" => Value::test_int(1),
223 }),
224 Value::test_record(record! {
225 "b" => Value::test_list(
226 vec![Value::test_int(1), Value::test_int(2)],),
227 }),
228 ])),
229 },
230 ]
231}
232
233fn from_yaml(input: PipelineData, head: Span) -> Result<PipelineData, ShellError> {
234 let (concat_string, span, metadata) = input.collect_string_strict(head)?;
235
236 match from_yaml_string_to_value(&concat_string, head, span) {
237 Ok(x) => {
238 Ok(x.into_pipeline_data_with_metadata(metadata.map(|md| md.with_content_type(None))))
239 }
240 Err(other) => Err(other),
241 }
242}
243
244#[cfg(test)]
245mod test {
246 use crate::Reject;
247 use crate::{Metadata, MetadataSet};
248
249 use super::*;
250 use nu_cmd_lang::eval_pipeline_without_terminal_expression;
251 use nu_protocol::Config;
252
253 #[test]
254 fn test_problematic_yaml() {
255 struct TestCase {
256 description: &'static str,
257 input: &'static str,
258 expected: Result<Value, ShellError>,
259 }
260 let tt: Vec<TestCase> = vec![
261 TestCase {
262 description: "Double Curly Braces With Quotes",
263 input: r#"value: "{{ something }}""#,
264 expected: Ok(Value::test_record(record! {
265 "value" => Value::test_string("{{ something }}"),
266 })),
267 },
268 TestCase {
269 description: "Double Curly Braces Without Quotes",
270 input: r#"value: {{ something }}"#,
271 expected: Ok(Value::test_record(record! {
272 "value" => Value::test_string("{{ something }}"),
273 })),
274 },
275 ];
276 let config = Config::default();
277 for tc in tt {
278 let actual = from_yaml_string_to_value(tc.input, Span::test_data(), Span::test_data());
279 if actual.is_err() {
280 assert!(
281 tc.expected.is_err(),
282 "actual is Err for test:\nTest Description {}\nErr: {:?}",
283 tc.description,
284 actual
285 );
286 } else {
287 assert_eq!(
288 actual.unwrap().to_expanded_string("", &config),
289 tc.expected.unwrap().to_expanded_string("", &config)
290 );
291 }
292 }
293 }
294
295 #[test]
296 fn test_examples() {
297 use crate::test_examples;
298
299 test_examples(FromYaml {})
300 }
301
302 #[test]
303 fn test_consistent_mapping_ordering() {
304 let test_yaml = "- a: b
305 b: c
306- a: g
307 b: h";
308
309 for ii in 1..1000 {
313 let actual = from_yaml_string_to_value(test_yaml, Span::test_data(), Span::test_data());
314
315 let expected: Result<Value, ShellError> = Ok(Value::test_list(vec![
316 Value::test_record(record! {
317 "a" => Value::test_string("b"),
318 "b" => Value::test_string("c"),
319 }),
320 Value::test_record(record! {
321 "a" => Value::test_string("g"),
322 "b" => Value::test_string("h"),
323 }),
324 ]));
325
326 assert!(actual.is_ok());
330 let actual = actual.ok().unwrap();
331 let expected = expected.ok().unwrap();
332
333 let actual_vals = actual.as_list().unwrap();
334 let expected_vals = expected.as_list().unwrap();
335 assert_eq!(expected_vals.len(), actual_vals.len(), "iteration {ii}");
336
337 for jj in 0..expected_vals.len() {
338 let actual_record = actual_vals[jj].as_record().unwrap();
339 let expected_record = expected_vals[jj].as_record().unwrap();
340
341 let actual_columns = actual_record.columns();
342 let expected_columns = expected_record.columns();
343 assert!(
344 expected_columns.eq(actual_columns),
345 "record {jj}, iteration {ii}"
346 );
347
348 let actual_vals = actual_record.values();
349 let expected_vals = expected_record.values();
350 assert!(expected_vals.eq(actual_vals), "record {jj}, iteration {ii}")
351 }
352 }
353 }
354
355 #[test]
356 fn test_convert_yaml_value_to_nu_value_for_tagged_values() {
357 struct TestCase {
358 input: &'static str,
359 expected: Result<Value, ShellError>,
360 }
361
362 let test_cases: Vec<TestCase> = vec![
363 TestCase {
364 input: "Key: !Value ${TEST}-Test-role",
365 expected: Ok(Value::test_record(record! {
366 "Key" => Value::test_string("!Value ${TEST}-Test-role"),
367 })),
368 },
369 TestCase {
370 input: "Key: !Value test-${TEST}",
371 expected: Ok(Value::test_record(record! {
372 "Key" => Value::test_string("!Value test-${TEST}"),
373 })),
374 },
375 TestCase {
376 input: "Key: !Value",
377 expected: Ok(Value::test_record(record! {
378 "Key" => Value::test_string("!Value"),
379 })),
380 },
381 TestCase {
382 input: "Key: !True",
383 expected: Ok(Value::test_record(record! {
384 "Key" => Value::test_string("!True"),
385 })),
386 },
387 TestCase {
388 input: "Key: !123",
389 expected: Ok(Value::test_record(record! {
390 "Key" => Value::test_string("!123"),
391 })),
392 },
393 ];
394
395 for test_case in test_cases {
396 let doc = serde_yaml::Deserializer::from_str(test_case.input);
397 let v: serde_yaml::Value = serde_yaml::Value::deserialize(doc.last().unwrap()).unwrap();
398 let result = convert_yaml_value_to_nu_value(&v, Span::test_data(), Span::test_data());
399 assert!(result.is_ok());
400 assert!(result.ok().unwrap() == test_case.expected.ok().unwrap());
401 }
402 }
403
404 #[test]
405 fn test_content_type_metadata() {
406 let mut engine_state = Box::new(EngineState::new());
407 let delta = {
408 let mut working_set = StateWorkingSet::new(&engine_state);
409
410 working_set.add_decl(Box::new(FromYaml {}));
411 working_set.add_decl(Box::new(Metadata {}));
412 working_set.add_decl(Box::new(MetadataSet {}));
413 working_set.add_decl(Box::new(Reject {}));
414
415 working_set.render()
416 };
417
418 engine_state
419 .merge_delta(delta)
420 .expect("Error merging delta");
421
422 let cmd = r#""a: 1\nb: 2" | metadata set --content-type 'application/yaml' --datasource-ls | from yaml | metadata | reject span | $in"#;
423 let result = eval_pipeline_without_terminal_expression(
424 cmd,
425 std::env::temp_dir().as_ref(),
426 &mut engine_state,
427 );
428 assert_eq!(
429 Value::test_record(record!("source" => Value::test_string("ls"))),
430 result.expect("There should be a result")
431 )
432 }
433}