Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[JSONSelection] Support conditional ... fragment selections #6188

Draft
wants to merge 2 commits into
base: next
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
}
}
Comment on lines +321 to +328
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling out this TODO as a topic we've previously discussed, but that becomes more urgent with conditional fragments: how should we merge fields (potentially coming from different fragments, with different subselections) when they collide?

};

(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
Loading