Skip to content

Commit

Permalink
Support ->has method for array, string, and object indexes/keys.
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamn committed Aug 2, 2024
1 parent e9e5ba0 commit 61566c7
Show file tree
Hide file tree
Showing 2 changed files with 209 additions and 16 deletions.
31 changes: 30 additions & 1 deletion apollo-federation/src/sources/connect/json_selection/apply_to.rs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,36 @@ impl ApplyTo for PathSelection {
input_path: &InputPath<JSON>,
errors: &mut IndexSet<ApplyToError>,
) -> Option<JSON> {
self.path.apply_to_path(data, vars, input_path, errors)
match &self.path {
// If this is a KeyPath, instead of using data as given, we need to
// evaluate the path starting from the current value of $. To
// evaluate the KeyPath against data, prefix it with @. This logic
// supports method chaining like obj->has('a')->and(obj->has('b')),
// where both obj references are interpreted as $.obj.
PathList::Key(key, tail) => {
if let Some((dollar_data, dollar_path)) = vars.get("$") {
let input_path_with_key = dollar_path.append(key.to_json());
if let Some(child) = dollar_data.get(key.as_string()) {
tail.apply_to_path(child, vars, &input_path_with_key, errors)
} else {
errors.insert(ApplyToError::new(
format!(
"Property {} not found in {}",
key.dotted(),
json_type_name(dollar_data),
)
.as_str(),
input_path_with_key.to_vec(),
));
None
}
} else {
// If $ is undefined for some reason, fall back to using data.
self.path.apply_to_path(data, vars, input_path, errors)
}
}
path => path.apply_to_path(data, vars, input_path, errors),
}
}
}

Expand Down
194 changes: 179 additions & 15 deletions apollo-federation/src/sources/connect/json_selection/methods.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ lazy_static! {
// Array/string methods
methods.insert("first".to_string(), first_method);
methods.insert("last".to_string(), last_method);
methods.insert("has".to_string(), has_method);
methods.insert("get".to_string(), get_method);
methods.insert("slice".to_string(), slice_method);
// The ->size method works for objects as well as arrays and strings.
Expand Down Expand Up @@ -465,6 +466,87 @@ fn last_method(
}
}

fn has_method(
method_name: &str,
method_args: &Option<MethodArgs>,
data: &JSON,
vars: &VarsWithPathsMap,
input_path: &InputPath<JSON>,
tail: &PathList,
errors: &mut IndexSet<ApplyToError>,
) -> Option<JSON> {
if let Some(MethodArgs(args)) = method_args {
match args.first() {
Some(arg) => match &arg.apply_to_path(data, vars, input_path, errors) {
Some(json_index @ JSON::Number(n)) => match (data, n.as_i64()) {
(JSON::Array(array), Some(index)) => {
let ilen = array.len() as i64;
// Negative indices count from the end of the array
let index = if index < 0 { ilen + index } else { index };
tail.apply_to_path(
&JSON::Bool(index >= 0 && index < ilen),
vars,
&input_path.append(json_index.clone()),
errors,
)
}
(json_key @ JSON::String(s), Some(index)) => {
let ilen = s.as_str().len() as i64;
// Negative indices count from the end of the array
let index = if index < 0 { ilen + index } else { index };
tail.apply_to_path(
&JSON::Bool(index >= 0 && index < ilen),
vars,
&input_path.append(json_key.clone()),
errors,
)
}
_ => tail.apply_to_path(
&JSON::Bool(false),
vars,
&input_path.append(json_index.clone()),
errors,
),
},
Some(json_key @ JSON::String(s)) => match data {
JSON::Object(map) => tail.apply_to_path(
&JSON::Bool(map.contains_key(s.as_str())),
vars,
&input_path.append(json_key.clone()),
errors,
),
_ => tail.apply_to_path(
&JSON::Bool(false),
vars,
&input_path.append(json_key.clone()),
errors,
),
},
Some(value) => tail.apply_to_path(
&JSON::Bool(false),
vars,
&input_path.append(value.clone()),
errors,
),
None => tail.apply_to_path(&JSON::Bool(false), vars, input_path, errors),
},
None => {
errors.insert(ApplyToError::new(
format!("Method ->{} requires an argument", method_name).as_str(),
input_path.to_vec(),
));
None
}
}
} else {
errors.insert(ApplyToError::new(
format!("Method ->{} requires an argument", method_name).as_str(),
input_path.to_vec(),
));
None
}
}

// Returns the array or string element at the given index, as Option<JSON>. If
// the index is out of bounds, returns None and reports an error.
fn get_method(
Expand Down Expand Up @@ -545,42 +627,34 @@ fn get_method(
));
None
}
}
},
Some(key @ JSON::String(s)) => match data {
JSON::Object(map) => {
if let Some(value) = map.get(s.as_str()) {
tail.apply_to_path(value, vars, input_path, errors)
} else {
errors.insert(ApplyToError::new(
format!(
"Method ->{}({}) object key not found",
method_name, key,
)
.as_str(),
format!("Method ->{}({}) object key not found", method_name, key,)
.as_str(),
input_path.to_vec(),
));
None
}
}
_ => {
errors.insert(ApplyToError::new(
format!(
"Method ->{}({}) requires an object input",
method_name,
key,
)
.as_str(),
format!("Method ->{}({}) requires an object input", method_name, key,)
.as_str(),
input_path.to_vec(),
));
None
}
}
},
Some(value) => {
errors.insert(ApplyToError::new(
format!(
"Method ->{}({}) requires an integer or string argument",
method_name,
value,
method_name, value,
)
.as_str(),
input_path.to_vec(),
Expand Down Expand Up @@ -1407,6 +1481,96 @@ mod tests {
),
);

// Test the ->has method
assert_eq!(
selection!("$->has(1)").apply_to(&json!([1, 2, 3])),
(Some(json!(true)), vec![]),
);
assert_eq!(
selection!("$->has(5)").apply_to(&json!([1, 2, 3])),
(Some(json!(false)), vec![]),
);
assert_eq!(
selection!("$->has(2)").apply_to(&json!("oyez")),
(Some(json!(true)), vec![]),
);
assert_eq!(
selection!("$->has(-2)").apply_to(&json!("oyez")),
(Some(json!(true)), vec![]),
);
assert_eq!(
selection!("$->has(10)").apply_to(&json!("oyez")),
(Some(json!(false)), vec![]),
);
assert_eq!(
selection!("$->has(-10)").apply_to(&json!("oyez")),
(Some(json!(false)), vec![]),
);
// Test the ->has method with object keys
assert_eq!(
selection!("object->has('a')").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(true)), vec![]),
);
assert_eq!(
selection!("object->has('c')").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(false)), vec![]),
);
assert_eq!(
selection!("object->has(true)").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(false)), vec![]),
);
assert_eq!(
selection!("object->has(null)").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(false)), vec![]),
);
assert_eq!(
selection!("object->has('a')->and(object->has('b'))").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(true)), vec![]),
);
assert_eq!(
selection!("object->has('b')->and(object->has('c'))").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!(false)), vec![]),
);
assert_eq!(
selection!("object->has('xxx')->typeof").apply_to(&json!({
"object": {
"a": 123,
"b": 456,
},
})),
(Some(json!("boolean")), vec![]),
);

assert_eq!(
selection!("$->slice(1, 3)").apply_to(&json!([1, 2, 3, 4, 5])),
(Some(json!([2, 3])), vec![]),
Expand Down

0 comments on commit 61566c7

Please sign in to comment.