use serde_json_bytes::Value as JSON;
use shape::Shape;
use crate::connectors::json_selection::ApplyToError;
use crate::connectors::json_selection::ApplyToInternal;
use crate::connectors::json_selection::MethodArgs;
use crate::connectors::json_selection::ShapeContext;
use crate::connectors::json_selection::VarsWithPathsMap;
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!(HasMethod, has_method, has_shape);
fn has_method(
method_name: &WithRange<String>,
method_args: Option<&MethodArgs>,
data: &JSON,
vars: &VarsWithPathsMap,
input_path: &InputPath<JSON>,
spec: ConnectSpec,
) -> (Option<JSON>, Vec<ApplyToError>) {
let Some(arg) = method_args.and_then(|MethodArgs { args, .. }| args.first()) else {
return (
None,
vec![ApplyToError::new(
format!("Method ->{} requires an argument", method_name.as_ref()),
input_path.to_vec(),
method_name.range(),
spec,
)],
);
};
match arg.apply_to_path(data, vars, input_path, spec) {
(Some(JSON::Number(ref n)), arg_errors) => {
match (data, n.as_i64()) {
(JSON::Array(array), Some(index)) => {
let ilen = array.len() as i64;
let index = if index < 0 { ilen + index } else { index };
(Some(JSON::Bool(index >= 0 && index < ilen)), arg_errors)
}
(JSON::String(s), Some(index)) => {
let ilen = s.as_str().len() as i64;
let index = if index < 0 { ilen + index } else { index };
(Some(JSON::Bool(index >= 0 && index < ilen)), arg_errors)
}
_ => (Some(JSON::Bool(false)), arg_errors),
}
}
(Some(JSON::String(ref s)), arg_errors) => match data {
JSON::Object(map) => (Some(JSON::Bool(map.contains_key(s.as_str()))), arg_errors),
_ => (Some(JSON::Bool(false)), arg_errors),
},
(None, arg_errors) => (None, arg_errors),
(Some(_), arg_errors) => (Some(JSON::Bool(false)), arg_errors),
}
}
#[allow(dead_code)] fn has_shape(
context: &ShapeContext,
method_name: &WithRange<String>,
_method_args: Option<&MethodArgs>,
_input_shape: Shape,
_dollar_shape: Shape,
) -> Shape {
Shape::bool(method_name.shape_location(context.source_id()))
}
#[cfg(test)]
mod tests {
use serde_json_bytes::json;
use crate::connectors::ConnectSpec;
use crate::connectors::json_selection::ApplyToError;
use crate::selection;
#[test]
fn has_should_return_true_when_array_has_item_at_specified_index() {
assert_eq!(
selection!("$->has(1)").apply_to(&json!([1, 2, 3])),
(Some(json!(true)), vec![]),
);
}
#[test]
fn has_should_return_false_when_array_does_not_have_item_at_specified_index() {
assert_eq!(
selection!("$->has(5)").apply_to(&json!([1, 2, 3])),
(Some(json!(false)), vec![]),
);
}
#[test]
fn has_should_return_true_when_string_has_character_at_specified_index() {
assert_eq!(
selection!("$->has(2)").apply_to(&json!("oyez")),
(Some(json!(true)), vec![]),
);
}
#[test]
fn has_should_return_true_when_string_has_character_at_specified_negative_index() {
assert_eq!(
selection!("$->has(-2)").apply_to(&json!("oyez")),
(Some(json!(true)), vec![]),
);
}
#[test]
fn has_should_return_false_when_string_does_not_have_character_at_specified_negative_index() {
assert_eq!(
selection!("$->has(10)").apply_to(&json!("oyez")),
(Some(json!(false)), vec![]),
);
}
#[test]
fn has_should_return_true_when_object_has_specified_property() {
assert_eq!(
selection!("object->has('a')").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(true)), vec![]),
);
}
#[test]
fn has_should_return_false_when_object_does_not_have_specified_property() {
assert_eq!(
selection!("object->has('c')").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(false)), vec![]),
);
}
#[test]
fn has_should_return_false_when_trying_to_access_boolean_property_name() {
assert_eq!(
selection!("object->has(true)").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(false)), vec![]),
);
}
#[test]
fn has_should_return_false_when_trying_to_access_null_property_name() {
assert_eq!(
selection!("object->has(null)").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(false)), vec![]),
);
}
#[test]
fn has_should_return_boolean_type() {
assert_eq!(
selection!("object->has('xxx')->typeof").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!("boolean")), vec![]),
);
}
#[rstest::rstest]
#[case::v0_2(ConnectSpec::V0_2)]
#[case::v0_3(ConnectSpec::V0_3)]
#[case::v0_4(ConnectSpec::V0_4)]
fn has_should_return_none_when_argument_evaluates_to_none(#[case] spec: ConnectSpec) {
assert_eq!(
selection!("obj->has($.missing)", spec).apply_to(&json!({
"obj": [1, 2, 3]
})),
(
None,
vec![ApplyToError::from_json(&json!({
"message": "Property .missing not found in object",
"path": ["missing"],
"range": [11, 18],
"spec": spec.to_string(),
}))]
),
);
}
}