Skip to content

Commit

Permalink
[JSONSelection] Support conditional fragment selections.
Browse files Browse the repository at this point in the history
  • Loading branch information
benjamn committed Oct 23, 2024
1 parent 506a282 commit b0a89c2
Show file tree
Hide file tree
Showing 14 changed files with 1,679 additions and 15 deletions.
40 changes: 39 additions & 1 deletion apollo-federation/src/sources/connect/json_selection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,18 @@ below.
```ebnf
JSONSelection ::= PathSelection | NamedSelection*
SubSelection ::= "{" NamedSelection* "}"
NamedSelection ::= NamedPathSelection | PathWithSubSelection | NamedFieldSelection | NamedGroupSelection
NamedSelection ::= NamedPathSelection
| PathWithSubSelection
| NamedFieldSelection
| NamedGroupSelection
| ConditionalSelection
NamedPathSelection ::= Alias PathSelection
NamedFieldSelection ::= Alias? Key SubSelection?
NamedGroupSelection ::= Alias SubSelection
Alias ::= Key ":"
ConditionalSelection ::= "..." ConditionalTest
ConditionalTest ::= "if" "(" Path ")" SubSelection ConditionalElse?
ConditionalElse ::= "else" (ConditionalTest | SubSelection)
Path ::= VarPath | KeyPath | AtPath | ExprPath
PathSelection ::= Path SubSelection?
PathWithSubSelection ::= Path SubSelection
Expand Down Expand Up @@ -351,6 +358,37 @@ from the input JSON to match the desired output shape.
In addition to renaming, `Alias` can provide names to otherwise anonymous
structures, such as those selected by `PathSelection` or `NamedGroupSelection`.

### `ConditionalSelection ::=`

![ConditionalSelection](./grammar/ConditionalSelection.svg)

The `...` token signifies the beginning of a `ConditionalSelection` element,
which is a kind of named selection that may appear multiple times within any
`SubSelection`.

The `...` is always followed by a `ConditionalTest`, since unconditional spreads
are rarely useful in this language, and can almost always be rewritten by
unwrapping the fields and removing the `...`.

### `ConditionalTest ::=`

![ConditionalTest](./grammar/ConditionalTest.svg)

`ConditionalTest` uses the `if` keyword followed by a parenthesized `Path` that
should evaluate to a boolean value. The `Path` is used to determine whether the
`SubSelection` or `ConditionalElse` should be selected.

### `ConditionalElse ::=`

![ConditionalElse](./grammar/ConditionalElse.svg)

`ConditionalElse` is an optional trailing clause of `ConditionalTest`, which
allows for typical `if`/`else`-style boolean control flow.

Note that `ConditionalElse` may expand to an `else` keyword followed by a
`ConditionalTest`, so the `ConditionalTest` and `ConditionalElse` rules are
mutually recursive.

### `Path ::=`

![Path](./grammar/Path.svg)
Expand Down
164 changes: 164 additions & 0 deletions apollo-federation/src/sources/connect/json_selection/apply_to.rs
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ impl ApplyToInternal for NamedSelection {
));
}
}

Self::Path(alias_opt, path_selection) => {
let (value_opt, apply_errors) =
path_selection.apply_to_path(data, vars, input_path);
Expand Down Expand Up @@ -308,19 +309,67 @@ impl ApplyToInternal for NamedSelection {
));
}
}

Self::Group(alias, sub_selection) => {
let (value_opt, apply_errors) = sub_selection.apply_to_path(data, vars, input_path);
errors.extend(apply_errors);
if let Some(value) = value_opt {
output.insert(alias.name(), value);
}
}

Self::Spread(spread) => {
let (spread_opt, spread_errors) = spread.apply_to_path(data, vars, input_path);
errors.extend(spread_errors);
if let Some(JSON::Object(spread)) = spread_opt {
// TODO Better merge strategy for conflicting fields
output.extend(spread);
}
}
};

(Some(JSON::Object(output)), errors)
}
}

impl ApplyToInternal for ConditionalTest {
fn apply_to_path(
&self,
data: &JSON,
vars: &VarsWithPathsMap,
input_path: &InputPath<JSON>,
) -> (Option<JSON>, Vec<ApplyToError>) {
let (test_value, test_errors) = self.test.apply_to_path(data, vars, input_path);
if let Some(JSON::Bool(true)) = test_value {
self.when_true
.apply_to_path(data, vars, input_path)
.prepend_errors(test_errors)
} else if let Some(when_else) = &self.when_else {
when_else
.apply_to_path(data, vars, input_path)
.prepend_errors(test_errors)
} else {
(None, test_errors)
}
}
}

impl ApplyToInternal for ConditionalElse {
fn apply_to_path(
&self,
data: &JSON,
vars: &VarsWithPathsMap,
input_path: &InputPath<JSON>,
) -> (Option<JSON>, Vec<ApplyToError>) {
match self {
Self::Else(selection) => selection.apply_to_path(data, vars, input_path),
Self::ElseIf(conditional_test) => {
conditional_test.apply_to_path(data, vars, input_path)
}
}
}
}

impl ApplyToInternal for PathSelection {
fn apply_to_path(
&self,
Expand Down Expand Up @@ -2055,4 +2104,119 @@ mod tests {
(Some(json!(123)), vec![],),
);
}

#[test]
fn test_conditional_fragments() {
let book = json!({
"kind": "book",
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald",
});

let movie = json!({
"kind": "movie",
"title": "The Great Gatsby",
"director": "Baz Luhrmann",
});

let product = json!({
"kind": "product",
"title": "Jay Gatsby Action Figure",
"price": 19.99,
});

let selection = selection!(
r#"
title
... if (kind->eq("book")) {
author
} else if (kind->eq("movie")) {
director
} else {
kind
}
"#
);

assert_eq!(
selection.apply_to(&book),
(
Some(json!({
"title": "The Great Gatsby",
"author": "F. Scott Fitzgerald"
})),
vec![]
)
);
assert_eq!(
selection.apply_to(&movie),
(
Some(json!({
"title": "The Great Gatsby",
"director": "Baz Luhrmann"
})),
vec![]
)
);
assert_eq!(
selection.apply_to(&product),
(
Some(json!({
"title": "Jay Gatsby Action Figure",
"kind": "product"
})),
vec![]
)
);

let selection_with_typenames = selection!(
r#"
title
... if (kind->eq("book")) {
__typename: $("Book")
author
} else if (kind->eq("movie")) {
__typename: $("Movie")
director
} else {
__typename: $("Product")
kind
}
"#
);

assert_eq!(
selection_with_typenames.apply_to(&book),
(
Some(json!({
"title": "The Great Gatsby",
"__typename": "Book",
"author": "F. Scott Fitzgerald"
})),
vec![]
)
);
assert_eq!(
selection_with_typenames.apply_to(&movie),
(
Some(json!({
"title": "The Great Gatsby",
"__typename": "Movie",
"director": "Baz Luhrmann"
})),
vec![]
)
);
assert_eq!(
selection_with_typenames.apply_to(&product),
(
Some(json!({
"title": "Jay Gatsby Action Figure",
"__typename": "Product",
"kind": "product"
})),
vec![]
)
);
}
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit b0a89c2

Please sign in to comment.