1use nu_engine::command_prelude::*;
2use nu_heavy_utils::yaml::{NonRoundtrip, SerializeOptions};
3
4#[derive(Clone)]
5pub struct ToYamlLike(&'static str);
6pub const TO_YAML: ToYamlLike = ToYamlLike("to yaml");
7pub const TO_YML: ToYamlLike = ToYamlLike("to yml");
8
9impl Command for ToYamlLike {
10 fn name(&self) -> &str {
11 self.0
12 }
13
14 fn signature(&self) -> Signature {
15 Signature::build(self.name())
16 .input_output_types(vec![(Type::Any, Type::String)])
17 .switch(
18 "serialize",
19 "Serialize nushell types that cannot be deserialized.",
20 Some('s'),
21 )
22 .param(
23 Flag::new("non-roundtrip")
24 .arg(SyntaxShape::String)
25 .desc("How to handle values that are non-roundtrippable.")
26 .completion(Completion::new_list(&["error", "null", "lossy"])),
27 )
28 .param(
29 Flag::new("spec")
30 .arg(SyntaxShape::String)
31 .desc("YAML spec version ('1.1' or '1.2' (default)).")
32 .completion(Completion::new_list(&["1.1", "1.2"])),
33 )
34 .switch("add-directives", "Add YAML document directives.", Some('d'))
35 .switch(
36 "multiple",
37 "Given a list, serialize a multi document stream.",
38 Some('m'),
39 )
40 .named(
41 "indent",
42 SyntaxShape::Int,
43 "Configure the indent.",
44 Some('i'),
45 )
46 .switch(
47 "compact-list-indent",
48 "Emit lists with a more compact indentation style.",
49 None,
50 )
51 .param(
52 Flag::new("quote")
53 .short('q')
54 .arg(SyntaxShape::String)
55 .desc("String quote style ('auto' (default), 'single' or 'double')")
56 .completion(Completion::new_list(&["auto", "single", "double"])),
57 )
58 .category(Category::Formats)
59 }
60
61 fn description(&self) -> &str {
62 "Convert table into .yaml/.yml text."
63 }
64
65 fn examples(&self) -> Vec<Example<'_>> {
66 vec![
67 Example {
68 description: "Outputs a YAML string representing the contents of this table.",
69 example: match self.name() {
70 "to yaml" => r#"[[foo bar]; ["1" "2"]] | to yaml"#,
71 "to yml" => r#"[[foo bar]; ["1" "2"]] | to yml"#,
72 _ => unreachable!("only implemented for `yaml` and `yml`"),
73 },
74 result: Some(Value::test_string("- foo: \"1\"\n bar: \"2\"\n")),
75 },
76 Example {
77 description: "Convert a nushell specific type into YAML.",
78 example: match self.name() {
79 "to yaml" => "$.1.abc | to yaml",
80 "to yml" => "$.1.abc | to yml",
81 _ => unreachable!("only implemented for `yaml` and `yml`"),
82 },
83 result: Some(Value::test_string("!cell-path $.1.abc\n")),
84 },
85 ]
86 }
87
88 fn run(
89 &self,
90 engine_state: &EngineState,
91 stack: &mut Stack,
92 call: &Call,
93 mut input: PipelineData,
94 ) -> Result<PipelineData, ShellError> {
95 let metadata = input
96 .take_metadata()
97 .unwrap_or_default()
98 .with_content_type(Some(String::from("application/yaml")));
99 let value = input.into_value(call.head)?;
100 let spec = call.get_flag(engine_state, stack, "spec")?;
101 let add_directives = call.has_flag(engine_state, stack, "add-directives")?;
102 let multiple = call.has_flag(engine_state, stack, "multiple")?;
103 let indent = call.get_flag(engine_state, stack, "indent")?;
104 let compact_list_indent = call.get_flag(engine_state, stack, "compact-list-indent")?;
105 let quote_style = call.get_flag(engine_state, stack, "quote")?;
106 let non_roundtrip =
107 call.get_flag::<Spanned<String>>(engine_state, stack, "non-roundtrip")?;
108 let non_roundtrip = match (
109 call.has_flag(engine_state, stack, "serialize")?,
110 non_roundtrip.as_ref().map(|nr| nr.item.as_ref()),
111 ) {
112 (false, None | Some("error")) => NonRoundtrip::Error,
114 (true, None | Some("lossy")) => NonRoundtrip::Lossy {
115 engine_state: Box::new(engine_state.clone()),
116 },
117 (false, Some("null")) => NonRoundtrip::Null,
118 (false, Some(_)) => {
119 return Err(ShellError::IncompatibleParametersSingle {
120 msg: "expected `error`, `null` or `lossy`".into(),
121 span: non_roundtrip.expect("non_roundtrip is some").span,
122 });
123 }
124 (true, Some(_)) => {
125 return Err(ShellError::IncompatibleParameters {
126 left_message: "this is a shorthand to".into(),
127 left_span: call
128 .get_flag_span(stack, "serialize")
129 .expect("serialize is some"),
130 right_message: "this with `lossy`".into(),
131 right_span: non_roundtrip.expect("non_roundtrip is some").span,
132 });
133 }
134 };
135
136 match call.has_flag(engine_state, stack, "serialize")? {
137 true => NonRoundtrip::Lossy {
138 engine_state: Box::new(engine_state.clone()),
139 },
140 false => NonRoundtrip::Null,
141 };
142
143 let defaults = SerializeOptions::default();
144 let options = SerializeOptions::default()
145 .with_spec(spec.unwrap_or(defaults.spec))
146 .with_non_roundtrip(non_roundtrip)
147 .with_add_directives(add_directives)
148 .with_multiple(multiple)
149 .with_indent(indent.unwrap_or(defaults.indent))
150 .with_compact_list_indent(compact_list_indent.unwrap_or(defaults.compact_list_indent))
151 .with_quote_style(quote_style.unwrap_or(defaults.quote_style));
152
153 nu_heavy_utils::yaml::serialize(&value, call.head, options)
154 .map(|s| PipelineData::value(Value::string(s, call.head), metadata))
155 }
156}
157
158#[cfg(test)]
159mod tests {
160 use super::*;
161 use nu_test_support::prelude::{Result, *};
162
163 #[test]
164 fn test_examples() -> Result {
165 test().examples(TO_YAML)?;
166 test().examples(TO_YML)?;
167 Ok(())
168 }
169
170 #[test]
171 fn test_content_type_metadata() -> Result {
172 let code = "
173 {a: 1, b: 2}
174 | to yaml
175 | metadata
176 | get content_type
177 ";
178
179 test().run(code).expect_value_eq("application/yaml")
180 }
181}