1use nu_engine::command_prelude::*;
2use nu_protocol::{PipelineMetadata, ast::PathMember};
3
4#[derive(Clone)]
5pub struct ToJson;
6
7impl Command for ToJson {
8 fn name(&self) -> &str {
9 "to json"
10 }
11
12 fn signature(&self) -> Signature {
13 Signature::build("to json")
14 .input_output_types(vec![(Type::Any, Type::String)])
15 .switch(
16 "raw",
17 "remove all of the whitespace and trailing line ending",
18 Some('r'),
19 )
20 .named(
21 "indent",
22 SyntaxShape::Number,
23 "specify indentation width",
24 Some('i'),
25 )
26 .named(
27 "tabs",
28 SyntaxShape::Number,
29 "specify indentation tab quantity",
30 Some('t'),
31 )
32 .switch(
33 "serialize",
34 "serialize nushell types that cannot be deserialized",
35 Some('s'),
36 )
37 .category(Category::Formats)
38 }
39
40 fn description(&self) -> &str {
41 "Converts table data into JSON text."
42 }
43
44 fn run(
45 &self,
46 engine_state: &EngineState,
47 stack: &mut Stack,
48 call: &Call,
49 input: PipelineData,
50 ) -> Result<PipelineData, ShellError> {
51 let raw = call.has_flag(engine_state, stack, "raw")?;
52 let use_tabs = call.get_flag(engine_state, stack, "tabs")?;
53 let indent = call.get_flag(engine_state, stack, "indent")?;
54 let serialize_types = call.has_flag(engine_state, stack, "serialize")?;
55
56 let span = call.head;
57 let input = input.try_expand_range()?;
59 let value = input.into_value(span)?;
60 let json_value = value_to_json_value(engine_state, &value, span, serialize_types)?;
61
62 let json_result = if raw {
63 nu_json::to_string_raw(&json_value)
64 } else if let Some(tab_count) = use_tabs {
65 nu_json::to_string_with_tab_indentation(&json_value, tab_count)
66 } else if let Some(indent) = indent {
67 nu_json::to_string_with_indent(&json_value, indent)
68 } else {
69 nu_json::to_string(&json_value)
70 };
71
72 match json_result {
73 Ok(serde_json_string) => {
74 let res = Value::string(serde_json_string, span);
75 let metadata = PipelineMetadata {
76 data_source: nu_protocol::DataSource::None,
77 content_type: Some(mime::APPLICATION_JSON.to_string()),
78 };
79 Ok(PipelineData::Value(res, Some(metadata)))
80 }
81 _ => Err(ShellError::CantConvert {
82 to_type: "JSON".into(),
83 from_type: value.get_type().to_string(),
84 span,
85 help: None,
86 }),
87 }
88 }
89
90 fn examples(&self) -> Vec<Example> {
91 vec![
92 Example {
93 description: "Outputs a JSON string, with default indentation, representing the contents of this table",
94 example: "[a b c] | to json",
95 result: Some(Value::test_string("[\n \"a\",\n \"b\",\n \"c\"\n]")),
96 },
97 Example {
98 description: "Outputs a JSON string, with 4-space indentation, representing the contents of this table",
99 example: "[Joe Bob Sam] | to json --indent 4",
100 result: Some(Value::test_string(
101 "[\n \"Joe\",\n \"Bob\",\n \"Sam\"\n]",
102 )),
103 },
104 Example {
105 description: "Outputs an unformatted JSON string representing the contents of this table",
106 example: "[1 2 3] | to json -r",
107 result: Some(Value::test_string("[1,2,3]")),
108 },
109 ]
110 }
111}
112
113pub fn value_to_json_value(
114 engine_state: &EngineState,
115 v: &Value,
116 call_span: Span,
117 serialize_types: bool,
118) -> Result<nu_json::Value, ShellError> {
119 let span = v.span();
120 Ok(match v {
121 Value::Bool { val, .. } => nu_json::Value::Bool(*val),
122 Value::Filesize { val, .. } => nu_json::Value::I64(val.get()),
123 Value::Duration { val, .. } => nu_json::Value::I64(*val),
124 Value::Date { val, .. } => nu_json::Value::String(val.to_string()),
125 Value::Float { val, .. } => nu_json::Value::F64(*val),
126 Value::Int { val, .. } => nu_json::Value::I64(*val),
127 Value::Nothing { .. } => nu_json::Value::Null,
128 Value::String { val, .. } => nu_json::Value::String(val.to_string()),
129 Value::Glob { val, .. } => nu_json::Value::String(val.to_string()),
130 Value::CellPath { val, .. } => nu_json::Value::Array(
131 val.members
132 .iter()
133 .map(|x| match &x {
134 PathMember::String { val, .. } => Ok(nu_json::Value::String(val.clone())),
135 PathMember::Int { val, .. } => Ok(nu_json::Value::U64(*val as u64)),
136 })
137 .collect::<Result<Vec<nu_json::Value>, ShellError>>()?,
138 ),
139
140 Value::List { vals, .. } => {
141 nu_json::Value::Array(json_list(engine_state, vals, call_span, serialize_types)?)
142 }
143 Value::Error { error, .. } => return Err(*error.clone()),
144 Value::Closure { val, .. } => {
145 if serialize_types {
146 let closure_string = val.coerce_into_string(engine_state, span)?;
147 nu_json::Value::String(closure_string.to_string())
148 } else {
149 return Err(ShellError::UnsupportedInput {
150 msg: "closures are currently not deserializable (use --serialize to serialize as a string)".into(),
151 input: "value originates from here".into(),
152 msg_span: call_span,
153 input_span: span,
154 });
155 }
156 }
157 Value::Range { .. } => nu_json::Value::Null,
158 Value::Binary { val, .. } => {
159 nu_json::Value::Array(val.iter().map(|x| nu_json::Value::U64(*x as u64)).collect())
160 }
161 Value::Record { val, .. } => {
162 let mut m = nu_json::Map::new();
163 for (k, v) in &**val {
164 m.insert(
165 k.clone(),
166 value_to_json_value(engine_state, v, call_span, serialize_types)?,
167 );
168 }
169 nu_json::Value::Object(m)
170 }
171 Value::Custom { val, .. } => {
172 let collected = val.to_base_value(span)?;
173 value_to_json_value(engine_state, &collected, call_span, serialize_types)?
174 }
175 })
176}
177
178fn json_list(
179 engine_state: &EngineState,
180 input: &[Value],
181 call_span: Span,
182 serialize_types: bool,
183) -> Result<Vec<nu_json::Value>, ShellError> {
184 let mut out = vec![];
185
186 for value in input {
187 out.push(value_to_json_value(
188 engine_state,
189 value,
190 call_span,
191 serialize_types,
192 )?);
193 }
194
195 Ok(out)
196}
197
198#[cfg(test)]
199mod test {
200 use nu_cmd_lang::eval_pipeline_without_terminal_expression;
201
202 use crate::{Get, Metadata};
203
204 use super::*;
205
206 #[test]
207 fn test_examples() {
208 use crate::test_examples;
209
210 test_examples(ToJson {})
211 }
212
213 #[test]
214 fn test_content_type_metadata() {
215 let mut engine_state = Box::new(EngineState::new());
216 let delta = {
217 let mut working_set = StateWorkingSet::new(&engine_state);
220
221 working_set.add_decl(Box::new(ToJson {}));
222 working_set.add_decl(Box::new(Metadata {}));
223 working_set.add_decl(Box::new(Get {}));
224
225 working_set.render()
226 };
227
228 engine_state
229 .merge_delta(delta)
230 .expect("Error merging delta");
231
232 let cmd = "{a: 1 b: 2} | to json | metadata | get content_type";
233 let result = eval_pipeline_without_terminal_expression(
234 cmd,
235 std::env::temp_dir().as_ref(),
236 &mut engine_state,
237 );
238 assert_eq!(
239 Value::test_record(record!("content_type" => Value::test_string("application/json"))),
240 result.expect("There should be a result")
241 );
242 }
243}