1use std::io::{BufRead, Cursor};
2
3use nu_engine::command_prelude::*;
4use nu_protocol::{
5 ListStream, Signals,
6 shell_error::{generic::GenericError, io::IoError},
7};
8
9#[derive(Clone)]
10pub struct FromJson;
11
12impl Command for FromJson {
13 fn name(&self) -> &str {
14 "from json"
15 }
16
17 fn description(&self) -> &str {
18 "Convert JSON text into structured data."
19 }
20
21 fn signature(&self) -> nu_protocol::Signature {
22 Signature::build("from json")
23 .input_output_types(vec![(Type::String, Type::Any)])
24 .switch("objects", "Treat each line as a separate value.", Some('o'))
25 .switch(
26 "strict",
27 "Follow the json specification exactly.",
28 Some('s'),
29 )
30 .category(Category::Formats)
31 }
32
33 fn examples(&self) -> Vec<Example<'_>> {
34 vec![
35 Example {
36 example: r#"'{ "a": 1 }' | from json"#,
37 description: "Converts json formatted string to table.",
38 result: Some(Value::test_record(record! {
39 "a" => Value::test_int(1),
40 })),
41 },
42 Example {
43 example: r#"'{ "a": 1, "b": [1, 2] }' | from json"#,
44 description: "Converts json formatted string to table.",
45 result: Some(Value::test_record(record! {
46 "a" => Value::test_int(1),
47 "b" => Value::test_list(vec![Value::test_int(1), Value::test_int(2)]),
48 })),
49 },
50 Example {
51 example: r#"'{ "a": 1, "b": 2 }' | from json -s"#,
52 description: "Parse json strictly which will error on comments and trailing commas.",
53 result: Some(Value::test_record(record! {
54 "a" => Value::test_int(1),
55 "b" => Value::test_int(2),
56 })),
57 },
58 Example {
59 example: r#"'{ "a": 1 }
60{ "b": 2 }' | from json --objects"#,
61 description: "Parse a stream of line-delimited JSON values.",
62 result: Some(Value::test_list(vec![
63 Value::test_record(record! {"a" => Value::test_int(1)}),
64 Value::test_record(record! {"b" => Value::test_int(2)}),
65 ])),
66 },
67 ]
68 }
69
70 fn run(
71 &self,
72 engine_state: &EngineState,
73 stack: &mut Stack,
74 call: &Call,
75 mut input: PipelineData,
76 ) -> Result<PipelineData, ShellError> {
77 let span = call.head;
78
79 let strict = call.has_flag(engine_state, stack, "strict")?;
80 let metadata = input.take_metadata().map(|md| md.with_content_type(None));
81
82 if call.has_flag(engine_state, stack, "objects")? {
84 match input {
86 PipelineData::Value(Value::String { val, .. }, ..) => {
87 Ok(PipelineData::list_stream(
88 read_json_lines(
89 Cursor::new(val),
90 span,
91 strict,
92 engine_state.signals().clone(),
93 ),
94 metadata,
95 ))
96 }
97 PipelineData::ByteStream(stream, ..)
98 if stream.type_() != ByteStreamType::Binary =>
99 {
100 if let Some(reader) = stream.reader() {
101 Ok(PipelineData::list_stream(
102 read_json_lines(reader, span, strict, Signals::empty()),
103 metadata,
104 ))
105 } else {
106 Ok(PipelineData::empty())
107 }
108 }
109 _ => Err(ShellError::OnlySupportsThisInputType {
110 exp_input_type: "string".into(),
111 wrong_type: input.get_type().to_string(),
112 dst_span: call.head,
113 src_span: input.span().unwrap_or(call.head),
114 }),
115 }
116 } else {
117 let (string_input, span, ..) = input.collect_string_strict(span)?;
119
120 if string_input.is_empty() {
121 return Ok(Value::nothing(span).into_pipeline_data());
122 }
123
124 Ok(try_str_to_value(&string_input, span, strict)?
125 .into_pipeline_data_with_metadata(metadata))
126 }
127 }
128}
129
130fn read_json_lines(
132 input: impl BufRead + Send + 'static,
133 span: Span,
134 strict: bool,
135 signals: Signals,
136) -> ListStream {
137 let iter = input
138 .lines()
139 .filter(|line| line.as_ref().is_ok_and(|line| !line.trim().is_empty()) || line.is_err())
140 .map(move |line| {
141 let line = line.map_err(|err| IoError::new(err, span, None))?;
142 try_str_to_value(&line, span, strict)
143 })
144 .map(move |result| result.unwrap_or_else(|err| Value::error(err, span)));
145
146 ListStream::new(iter, span, signals)
147}
148
149pub fn try_str_to_value(input: &str, span: Span, strict: bool) -> Result<Value, ShellError> {
150 match strict {
151 true => try_str_to_value_impl(
152 input,
153 span,
154 |s| serde_json::from_str(s),
155 |err| err.is_syntax().then_some((err.line(), err.column())),
156 ),
157 false => try_str_to_value_impl(input, span, nu_json::from_str, |err| match err {
158 nu_json::Error::Syntax(_, row, col) => Some((*row, *col)),
159 _ => None,
160 }),
161 }
162}
163
164#[inline]
165fn try_str_to_value_impl<E: std::error::Error>(
166 input: &str,
167 span: Span,
168 parser: impl Fn(&str) -> Result<nu_json::Value, E>,
169 on_syntax_err: impl Fn(&E) -> Option<(usize, usize)>,
170) -> Result<Value, ShellError> {
171 match parser(input) {
172 Ok(value) => Ok(value.into_value(span)),
173 Err(err) => match on_syntax_err(&err) {
174 Some((row, col)) => {
175 let label = err.to_string();
176 let label_span = Span::from_row_column(row, col, input);
177 Err(ShellError::Generic(
178 GenericError::new(
179 "Error while parsing JSON text",
180 "error parsing JSON text",
181 span,
182 )
183 .with_inner([ShellError::OutsideSpannedLabeledError {
184 src: input.into(),
185 error: "Error while parsing JSON text".into(),
186 msg: label,
187 span: label_span,
188 }]),
189 ))
190 }
191 None => Err(ShellError::CantConvert {
192 to_type: format!("structured json data ({err})"),
193 from_type: "string".into(),
194 span,
195 help: None,
196 }),
197 },
198 }
199}
200
201#[cfg(test)]
202mod test {
203 use super::*;
204
205 #[test]
206 fn test_examples() -> nu_test_support::Result {
207 nu_test_support::test().examples(FromJson)
208 }
209}