nu_command/formats/to/
yaml.rs1use 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 .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 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}