nu_command/formats/to/
json.rs

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        // allow ranges to expand and turn into array
58        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            // Base functions that are needed for testing
218            // Try to keep this working set small to keep tests running as fast as possible
219            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}