1use nu_engine::command_prelude::*;
2use nu_protocol::{ast::PathMember, PipelineMetadata};
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:
94 "Outputs a JSON string, with default indentation, representing the contents of this table",
95 example: "[a b c] | to json",
96 result: Some(Value::test_string("[\n \"a\",\n \"b\",\n \"c\"\n]")),
97 },
98 Example {
99 description:
100 "Outputs a JSON string, with 4-space indentation, representing the contents of this table",
101 example: "[Joe Bob Sam] | to json --indent 4",
102 result: Some(Value::test_string("[\n \"Joe\",\n \"Bob\",\n \"Sam\"\n]")),
103 },
104 Example {
105 description:
106 "Outputs an unformatted JSON string representing the contents of this table",
107 example: "[1 2 3] | to json -r",
108 result: Some(Value::test_string("[1,2,3]")),
109 },
110 ]
111 }
112}
113
114pub fn value_to_json_value(
115 engine_state: &EngineState,
116 v: &Value,
117 call_span: Span,
118 serialize_types: bool,
119) -> Result<nu_json::Value, ShellError> {
120 let span = v.span();
121 Ok(match v {
122 Value::Bool { val, .. } => nu_json::Value::Bool(*val),
123 Value::Filesize { val, .. } => nu_json::Value::I64(val.get()),
124 Value::Duration { val, .. } => nu_json::Value::I64(*val),
125 Value::Date { val, .. } => nu_json::Value::String(val.to_string()),
126 Value::Float { val, .. } => nu_json::Value::F64(*val),
127 Value::Int { val, .. } => nu_json::Value::I64(*val),
128 Value::Nothing { .. } => nu_json::Value::Null,
129 Value::String { val, .. } => nu_json::Value::String(val.to_string()),
130 Value::Glob { val, .. } => nu_json::Value::String(val.to_string()),
131 Value::CellPath { val, .. } => nu_json::Value::Array(
132 val.members
133 .iter()
134 .map(|x| match &x {
135 PathMember::String { val, .. } => Ok(nu_json::Value::String(val.clone())),
136 PathMember::Int { val, .. } => Ok(nu_json::Value::U64(*val as u64)),
137 })
138 .collect::<Result<Vec<nu_json::Value>, ShellError>>()?,
139 ),
140
141 Value::List { vals, .. } => {
142 nu_json::Value::Array(json_list(engine_state, vals, call_span, serialize_types)?)
143 }
144 Value::Error { error, .. } => return Err(*error.clone()),
145 Value::Closure { val, .. } => {
146 if serialize_types {
147 let closure_string = val.coerce_into_string(engine_state, span)?;
148 nu_json::Value::String(closure_string.to_string())
149 } else {
150 return Err(ShellError::UnsupportedInput {
151 msg: "closures are currently not deserializable (use --serialize to serialize as a string)".into(),
152 input: "value originates from here".into(),
153 msg_span: call_span,
154 input_span: span,
155 });
156 }
157 }
158 Value::Range { .. } => nu_json::Value::Null,
159 Value::Binary { val, .. } => {
160 nu_json::Value::Array(val.iter().map(|x| nu_json::Value::U64(*x as u64)).collect())
161 }
162 Value::Record { val, .. } => {
163 let mut m = nu_json::Map::new();
164 for (k, v) in &**val {
165 m.insert(
166 k.clone(),
167 value_to_json_value(engine_state, v, call_span, serialize_types)?,
168 );
169 }
170 nu_json::Value::Object(m)
171 }
172 Value::Custom { val, .. } => {
173 let collected = val.to_base_value(span)?;
174 value_to_json_value(engine_state, &collected, call_span, serialize_types)?
175 }
176 })
177}
178
179fn json_list(
180 engine_state: &EngineState,
181 input: &[Value],
182 call_span: Span,
183 serialize_types: bool,
184) -> Result<Vec<nu_json::Value>, ShellError> {
185 let mut out = vec![];
186
187 for value in input {
188 out.push(value_to_json_value(
189 engine_state,
190 value,
191 call_span,
192 serialize_types,
193 )?);
194 }
195
196 Ok(out)
197}
198
199#[cfg(test)]
200mod test {
201 use nu_cmd_lang::eval_pipeline_without_terminal_expression;
202
203 use crate::{Get, Metadata};
204
205 use super::*;
206
207 #[test]
208 fn test_examples() {
209 use crate::test_examples;
210
211 test_examples(ToJson {})
212 }
213
214 #[test]
215 fn test_content_type_metadata() {
216 let mut engine_state = Box::new(EngineState::new());
217 let delta = {
218 let mut working_set = StateWorkingSet::new(&engine_state);
221
222 working_set.add_decl(Box::new(ToJson {}));
223 working_set.add_decl(Box::new(Metadata {}));
224 working_set.add_decl(Box::new(Get {}));
225
226 working_set.render()
227 };
228
229 engine_state
230 .merge_delta(delta)
231 .expect("Error merging delta");
232
233 let cmd = "{a: 1 b: 2} | to json | metadata | get content_type";
234 let result = eval_pipeline_without_terminal_expression(
235 cmd,
236 std::env::temp_dir().as_ref(),
237 &mut engine_state,
238 );
239 assert_eq!(
240 Value::test_record(record!("content_type" => Value::test_string("application/json"))),
241 result.expect("There should be a result")
242 );
243 }
244}