Skip to main content

nu_command/formats/to/
yaml.rs

1use nu_engine::command_prelude::*;
2use nu_protocol::ast::PathMember;
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            .category(Category::Formats)
23    }
24
25    fn description(&self) -> &str {
26        "Convert table into .yaml/.yml text."
27    }
28
29    fn examples(&self) -> Vec<Example<'_>> {
30        vec![Example {
31            description: "Outputs a YAML string representing the contents of this table.",
32            example: match self.name() {
33                "to yaml" => r#"[[foo bar]; ["1" "2"]] | to yaml"#,
34                "to yml" => r#"[[foo bar]; ["1" "2"]] | to yml"#,
35                _ => unreachable!("only implemented for `yaml` and `yml`"),
36            },
37            result: Some(Value::test_string("- foo: '1'\n  bar: '2'\n")),
38        }]
39    }
40
41    fn run(
42        &self,
43        engine_state: &EngineState,
44        stack: &mut Stack,
45        call: &Call,
46        input: PipelineData,
47    ) -> Result<PipelineData, ShellError> {
48        let head = call.head;
49        let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
50        let input = input.try_expand_range()?;
51
52        to_yaml(engine_state, input, head, serialize_types)
53    }
54}
55
56pub fn value_to_yaml_value(
57    engine_state: &EngineState,
58    v: &Value,
59    serialize_types: bool,
60) -> Result<serde_yaml::Value, ShellError> {
61    Ok(match &v {
62        Value::Bool { val, .. } => serde_yaml::Value::Bool(*val),
63        Value::Int { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)),
64        Value::Filesize { val, .. } => {
65            serde_yaml::Value::Number(serde_yaml::Number::from(val.get()))
66        }
67        Value::Duration { val, .. } => serde_yaml::Value::String(val.to_string()),
68        Value::Date { val, .. } => serde_yaml::Value::String(val.to_string()),
69        Value::Range { .. } => serde_yaml::Value::Null,
70        Value::Float { val, .. } => serde_yaml::Value::Number(serde_yaml::Number::from(*val)),
71        Value::String { val, .. } | Value::Glob { val, .. } => {
72            serde_yaml::Value::String(val.clone())
73        }
74        Value::Record { val, .. } => {
75            let mut m = serde_yaml::Mapping::new();
76            for (k, v) in &**val {
77                m.insert(
78                    serde_yaml::Value::String(k.clone()),
79                    value_to_yaml_value(engine_state, v, serialize_types)?,
80                );
81            }
82            serde_yaml::Value::Mapping(m)
83        }
84        Value::List { vals, .. } => {
85            let mut out = vec![];
86
87            for value in vals {
88                out.push(value_to_yaml_value(engine_state, value, serialize_types)?);
89            }
90
91            serde_yaml::Value::Sequence(out)
92        }
93        Value::Closure { val, .. } => {
94            if serialize_types {
95                let block = engine_state.get_block(val.block_id);
96                if let Some(span) = block.span {
97                    let contents_bytes = engine_state.get_span_contents(span);
98                    let contents_string = String::from_utf8_lossy(contents_bytes);
99                    serde_yaml::Value::String(contents_string.to_string())
100                } else {
101                    serde_yaml::Value::String(format!(
102                        "unable to retrieve block contents for yaml block_id {}",
103                        val.block_id.get()
104                    ))
105                }
106            } else {
107                serde_yaml::Value::Null
108            }
109        }
110        Value::Nothing { .. } => serde_yaml::Value::Null,
111        Value::Error { error, .. } => return Err(*error.clone()),
112        Value::Binary { val, .. } => serde_yaml::Value::Sequence(
113            val.iter()
114                .map(|x| serde_yaml::Value::Number(serde_yaml::Number::from(*x)))
115                .collect(),
116        ),
117        Value::CellPath { val, .. } => serde_yaml::Value::Sequence(
118            val.members
119                .iter()
120                .map(|x| match &x {
121                    PathMember::String { val, .. } => Ok(serde_yaml::Value::String(val.clone())),
122                    PathMember::Int { val, .. } => {
123                        Ok(serde_yaml::Value::Number(serde_yaml::Number::from(*val)))
124                    }
125                })
126                .collect::<Result<Vec<serde_yaml::Value>, ShellError>>()?,
127        ),
128        Value::Custom { .. } => serde_yaml::Value::Null,
129    })
130}
131
132fn to_yaml(
133    engine_state: &EngineState,
134    mut input: PipelineData,
135    head: Span,
136    serialize_types: bool,
137) -> Result<PipelineData, ShellError> {
138    let metadata = input
139        .take_metadata()
140        .unwrap_or_default()
141        // Per RFC-9512, application/yaml should be used
142        .with_content_type(Some("application/yaml".into()));
143    let value = input.into_value(head)?;
144
145    let yaml_value = value_to_yaml_value(engine_state, &value, serialize_types)?;
146    match serde_yaml::to_string(&yaml_value) {
147        Ok(serde_yaml_string) => {
148            Ok(Value::string(serde_yaml_string, head)
149                .into_pipeline_data_with_metadata(Some(metadata)))
150        }
151        _ => Ok(Value::error(
152            ShellError::CantConvert {
153                to_type: "YAML".into(),
154                from_type: value.get_type().to_string(),
155                span: head,
156                help: None,
157            },
158            head,
159        )
160        .into_pipeline_data_with_metadata(Some(metadata))),
161    }
162}
163
164#[cfg(test)]
165mod test {
166    use super::*;
167    use crate::{Get, Metadata};
168    use nu_cmd_lang::eval_pipeline_without_terminal_expression;
169
170    #[test]
171    fn test_examples() -> nu_test_support::Result {
172        nu_test_support::test().examples(TO_YAML)?;
173        nu_test_support::test().examples(TO_YML)
174    }
175
176    #[test]
177    fn test_content_type_metadata() {
178        let mut engine_state = Box::new(EngineState::new());
179        let delta = {
180            // Base functions that are needed for testing
181            // Try to keep this working set small to keep tests running as fast as possible
182            let mut working_set = StateWorkingSet::new(&engine_state);
183
184            working_set.add_decl(Box::new(TO_YAML));
185            working_set.add_decl(Box::new(Metadata {}));
186            working_set.add_decl(Box::new(Get {}));
187
188            working_set.render()
189        };
190
191        engine_state
192            .merge_delta(delta)
193            .expect("Error merging delta");
194
195        let cmd = "{a: 1 b: 2} | to yaml  | metadata | get content_type | $in";
196        let result = eval_pipeline_without_terminal_expression(
197            cmd,
198            std::env::temp_dir().as_ref(),
199            &mut engine_state,
200        );
201        assert_eq!(
202            Value::test_string("application/yaml"),
203            result.expect("There should be a result")
204        );
205    }
206}