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