use serde_json_bytes::Value as JSON;
use shape::Shape;
use crate::connectors::json_selection::ApplyToError;
use crate::connectors::json_selection::MethodArgs;
use crate::connectors::json_selection::ShapeContext;
use crate::connectors::json_selection::VarsWithPathsMap;
use crate::connectors::json_selection::helpers::json_type_name;
use crate::connectors::json_selection::immutable::InputPath;
use crate::connectors::json_selection::location::Ranged;
use crate::connectors::json_selection::location::WithRange;
use crate::connectors::spec::ConnectSpec;
use crate::impl_arrow_method;
impl_arrow_method!(JsonParseMethod, json_parse_method, json_parse_shape);
fn json_parse_method(
method_name: &WithRange<String>,
method_args: Option<&MethodArgs>,
data: &JSON,
_vars: &VarsWithPathsMap,
input_path: &InputPath<JSON>,
spec: ConnectSpec,
) -> (Option<JSON>, Vec<ApplyToError>) {
if method_args.is_some() {
return (
None,
vec![ApplyToError::new(
format!(
"Method ->{} does not take any arguments",
method_name.as_ref()
),
input_path.to_vec(),
method_name.range(),
spec,
)],
);
}
match data {
JSON::String(s) => match serde_json::from_str::<JSON>(s.as_str()) {
Ok(parsed) => (Some(parsed), Vec::new()),
Err(err) => (
None,
vec![ApplyToError::new(
format!(
"Method ->{} failed to parse JSON string: {}",
method_name.as_ref(),
err
),
input_path.to_vec(),
method_name.range(),
spec,
)],
),
},
_ => (
None,
vec![ApplyToError::new(
format!(
"Method ->{} requires a string input, got {}",
method_name.as_ref(),
json_type_name(data)
),
input_path.to_vec(),
method_name.range(),
spec,
)],
),
}
}
#[allow(dead_code)] fn json_parse_shape(
context: &ShapeContext,
method_name: &WithRange<String>,
_method_args: Option<&MethodArgs>,
_input_shape: Shape,
_dollar_shape: Shape,
) -> Shape {
Shape::unknown(method_name.shape_location(context.source_id()))
}
#[cfg(test)]
mod tests {
use apollo_compiler::collections::IndexMap;
use serde_json_bytes::json;
use super::*;
use crate::connectors::ApplyToError;
use crate::selection;
#[rstest::rstest]
#[case(json!("null"), json!(null), vec![])]
#[case(json!("true"), json!(true), vec![])]
#[case(json!("false"), json!(false), vec![])]
#[case(json!("42"), json!(42), vec![])]
#[case(json!("0"), json!(0), vec![])]
#[case(json!("-1"), json!(-1), vec![])]
#[case(json!("-99"), json!(-99), vec![])]
#[case(json!("10.8"), json!(10.8), vec![])]
#[case(json!("0.0"), json!(0.0), vec![])]
#[case(json!("-1.5"), json!(-1.5), vec![])]
#[case(json!("1e10"), json!(1e10), vec![])]
#[case(json!("2.5e-3"), json!(2.5e-3), vec![])]
#[case(json!("\"hello world\""), json!("hello world"), vec![])]
#[case(json!("\"\""), json!(""), vec![])]
#[case(json!("\"with \\\"escaped\\\" quotes\""), json!("with \"escaped\" quotes"), vec![])]
#[case(json!("\"line\\nbreak\""), json!("line\nbreak"), vec![])]
#[case(json!("\"tab\\there\""), json!("tab\there"), vec![])]
#[case(json!("\"back\\\\slash\""), json!("back\\slash"), vec![])]
#[case(json!("\"unicode \\u0041\""), json!("unicode A"), vec![])]
fn json_parse_should_parse_primitives(
#[case] input: JSON,
#[case] expected: JSON,
#[case] errors: Vec<ApplyToError>,
) {
assert_eq!(
selection!("$->jsonParse").apply_to(&input),
(Some(expected), errors),
);
}
#[rstest::rstest]
#[case(json!("[]"), json!([]), vec![])]
#[case(json!("[1,2,3]"), json!([1, 2, 3]), vec![])]
#[case(json!("[1, 2, 3]"), json!([1, 2, 3]), vec![])]
#[case(json!("[1,\"two\",true,null]"), json!([1, "two", true, null]), vec![])]
#[case(json!("[[1,2],[3,4]]"), json!([[1, 2], [3, 4]]), vec![])]
#[case(json!("[{\"a\":1},{\"b\":2}]"), json!([{"a": 1}, {"b": 2}]), vec![])]
#[case(json!("[null,null,null]"), json!([null, null, null]), vec![])]
fn json_parse_should_parse_arrays(
#[case] input: JSON,
#[case] expected: JSON,
#[case] errors: Vec<ApplyToError>,
) {
assert_eq!(
selection!("$->jsonParse").apply_to(&input),
(Some(expected), errors),
);
}
#[rstest::rstest]
#[case(json!("{}"), json!({}), vec![])]
#[case(json!("{\"key\":\"value\"}"), json!({"key": "value"}), vec![])]
#[case(json!("{\"a\":1,\"b\":2,\"c\":3}"), json!({"a": 1, "b": 2, "c": 3}), vec![])]
#[case(json!("{\"nested\":{\"deep\":{\"value\":true}}}"), json!({"nested": {"deep": {"value": true}}}), vec![])]
#[case(json!("{\"arr\":[1,2,3],\"obj\":{\"k\":\"v\"}}"), json!({"arr": [1, 2, 3], "obj": {"k": "v"}}), vec![])]
#[case(json!("{\"empty_arr\":[],\"empty_obj\":{}}"), json!({"empty_arr": [], "empty_obj": {}}), vec![])]
#[case(json!("{\"null_val\":null,\"bool_val\":false}"), json!({"null_val": null, "bool_val": false}), vec![])]
fn json_parse_should_parse_objects(
#[case] input: JSON,
#[case] expected: JSON,
#[case] errors: Vec<ApplyToError>,
) {
assert_eq!(
selection!("$->jsonParse").apply_to(&input),
(Some(expected), errors),
);
}
#[rstest::rstest]
#[case(json!(" 42 "), json!(42), vec![])]
#[case(json!("\t42\t"), json!(42), vec![])]
#[case(json!("\n42\n"), json!(42), vec![])]
#[case(json!(" true "), json!(true), vec![])]
#[case(json!(" null "), json!(null), vec![])]
#[case(json!(" { \"key\" : \"value\" } "), json!({"key": "value"}), vec![])]
#[case(json!(" [ 1 , 2 , 3 ] "), json!([1, 2, 3]), vec![])]
#[case(json!("\n{\n \"a\": 1,\n \"b\": 2\n}\n"), json!({"a": 1, "b": 2}), vec![])]
fn json_parse_should_handle_leading_and_trailing_whitespace(
#[case] input: JSON,
#[case] expected: JSON,
#[case] errors: Vec<ApplyToError>,
) {
assert_eq!(
selection!("$->jsonParse").apply_to(&input),
(Some(expected), errors),
);
}
#[rstest::rstest]
#[case(json!("not valid json"))]
#[case(json!(""))]
#[case(json!("{"))]
#[case(json!("["))]
#[case(json!("{\"key\":}"))]
#[case(json!("[1,2,]"))]
#[case(json!("'single quotes'"))]
#[case(json!("undefined"))]
#[case(json!("{key: value}"))]
fn json_parse_should_error_on_invalid_json(#[case] input: JSON) {
let result = selection!("$->jsonParse").apply_to(&input);
assert!(result.0.is_none());
assert_eq!(result.1.len(), 1);
assert!(
result.1[0]
.message()
.contains("failed to parse JSON string")
);
}
#[rstest::rstest]
#[case(json!(42), "number")]
#[case(json!(1.5), "number")]
#[case(json!(true), "boolean")]
#[case(json!(false), "boolean")]
#[case(json!(null), "null")]
#[case(json!([1, 2, 3]), "array")]
#[case(json!({"key": "value"}), "object")]
fn json_parse_should_error_on_non_string_input(
#[case] input: JSON,
#[case] expected_type: &str,
) {
let result = selection!("$->jsonParse").apply_to(&input);
assert!(result.0.is_none());
assert_eq!(result.1.len(), 1);
assert!(
result.1[0]
.message()
.contains(&format!("requires a string input, got {expected_type}"))
);
}
#[test]
fn json_parse_should_error_when_provided_argument() {
assert_eq!(
selection!("$->jsonParse(1)").apply_to(&json!("null")),
(
None,
vec![ApplyToError::new(
"Method ->jsonParse does not take any arguments".to_string(),
vec![json!("->jsonParse")],
Some(3..12),
ConnectSpec::latest(),
)],
),
);
}
#[rstest::rstest]
#[case(json!({ "key": [1, "two", true, null] }))]
#[case(json!(42))]
#[case(json!("hello"))]
#[case(json!(true))]
#[case(json!(null))]
#[case(json!([1, 2, 3]))]
#[case(json!({ "nested": { "deep": [1, { "x": true }] } }))]
fn json_stringify_then_json_parse_roundtrip(#[case] original: JSON) {
assert_eq!(
selection!("$->jsonStringify->jsonParse").apply_to(&original),
(Some(original), vec![]),
);
}
#[test]
fn json_parse_from_variable() {
let mut vars = IndexMap::default();
vars.insert(
"$encoded".to_string(),
json!("{\"id\":123,\"name\":\"Alice\"}"),
);
assert_eq!(
selection!("$encoded->jsonParse").apply_with_vars(&json!({}), &vars),
(Some(json!({"id": 123, "name": "Alice"})), vec![]),
);
}
#[test]
fn json_parse_from_variable_property() {
let mut vars = IndexMap::default();
vars.insert(
"$response".to_string(),
json!({"body": "{\"status\":\"ok\",\"count\":42}"}),
);
assert_eq!(
selection!("$response.body->jsonParse").apply_with_vars(&json!({}), &vars),
(Some(json!({"status": "ok", "count": 42})), vec![]),
);
}
#[test]
fn json_parse_from_nested_variable_property() {
let mut vars = IndexMap::default();
vars.insert("$data".to_string(), json!({"outer": {"inner": "[1,2,3]"}}));
assert_eq!(
selection!("$data.outer.inner->jsonParse").apply_with_vars(&json!({}), &vars),
(Some(json!([1, 2, 3])), vec![]),
);
}
#[test]
fn json_parse_from_data_property() {
let data = json!({"payload": "{\"key\":\"value\"}"});
assert_eq!(
selection!("payload->jsonParse").apply_to(&data),
(Some(json!({"key": "value"})), vec![]),
);
}
#[test]
fn json_parse_from_nested_data_property() {
let data = json!({"response": {"encoded": "true"}});
assert_eq!(
selection!("response.encoded->jsonParse").apply_to(&data),
(Some(json!(true)), vec![]),
);
}
#[test]
fn json_parse_then_select_into_parsed_result() {
let data = json!({"payload": "{\"users\":[{\"name\":\"Alice\"},{\"name\":\"Bob\"}]}"});
assert_eq!(
selection!("payload->jsonParse { users { name } }").apply_to(&data),
(
Some(json!({"users": [{"name": "Alice"}, {"name": "Bob"}]})),
vec![],
),
);
}
#[test]
fn json_parse_variable_with_roundtrip() {
let mut vars = IndexMap::default();
vars.insert("$input".to_string(), json!({"x": 1, "y": 2}));
assert_eq!(
selection!("$input->jsonStringify->jsonParse").apply_with_vars(&json!({}), &vars),
(Some(json!({"x": 1, "y": 2})), vec![]),
);
}
}