Skip to main content

nu_command/formats/to/
yaml.rs

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            // matching the spanned is way less comprehendible here, so we expect spans instead
113            (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}