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

Allow naked LitExpr::LitPath in expression parsing context #6784

Open
wants to merge 7 commits into
base: dev
Choose a base branch
from
101 changes: 86 additions & 15 deletions apollo-federation/src/sources/connect/json_selection/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ PathStep ::= "." Key | "->" Identifier MethodArgs?
Key ::= Identifier | LitString
Identifier ::= [a-zA-Z_] NO_SPACE [0-9a-zA-Z_]*
MethodArgs ::= "(" (LitExpr ("," LitExpr)* ","?)? ")"
LitExpr ::= LitPrimitive | LitObject | LitArray | PathSelection
LitExpr ::= LitPath | LitPrimitive | LitObject | LitArray | PathSelection
LitPath ::= (LitPrimitive | LitObject | LitArray) PathStep+
Comment on lines -102 to +103
Copy link
Member Author

Choose a reason for hiding this comment

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

It's important that LitPath is tried first in the list of LitExpr alternatives, because a LitPath always has a prefix that looks like one of the other JSON value types.

Copy link
Member Author

Choose a reason for hiding this comment

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

It's also important that we never allow a PathSelection to appear at the beginning of the LitPath (only (LitPrimitive | LitObject | LitArray)), because then the boundary between the PathSelection and the PathStep+ suffix would be ambiguous.

LitPrimitive ::= LitString | LitNumber | "true" | "false" | "null"
LitString ::= "'" ("\\'" | [^'])* "'" | '"' ('\\"' | [^"])* '"'
LitNumber ::= "-"? ([0-9]+ ("." [0-9]*)? | "." [0-9]+)
Expand Down Expand Up @@ -672,25 +673,46 @@ The `->echo` method is still useful when you want to do something with the input
value (which is bound to `@` within the echoed expression), rather than ignoring
the input value (using `@` nowhere in the expression).

The `$(...)` syntax can be useful within a `LitExpr` as well:
#### The difference between `array.field->map(...)` and `$(array.field)->map(...)`

```graphql
# $(-1) needs wrapping in order to apply the ->mul method
suffix: results.slice($(-1)->mul($args.suffixLength))
When you apply a field selection to an array (as in `array.field`), the field
selection is automatically mapped over each element of the array, producing a
new array of all the field values.

If the field selection has a further `->method` applied to it (as in
`array.field->map(...)`), the method will be applied to each of the resulting
field values _individually_, rather than to the array as a whole, which is
probably not what you want given that you're using `->map` (unless each field
value is an array, and you want an array of all those arrays, after mapping).

# Instead of something like this:
# suffix: results.slice($->echo(-1)->mul($args.suffixLength))
The `$(...)` wrapping syntax can be useful to control this behavior, because it
allows writing `$(array.field)->map(...)`, which provides the complete array of
field values as a single input to the `->map` method:

```json
// Input JSON
{
"array": [
{ "field": 1 },
{ "field": 2 },
{ "field": 3 }
]
}
```

In fairness, due to the commutavity of multiplication, this particular case
could have been written as `suffix: results.slice($args.suffixLength->mul(-1))`,
but not all methods allow reversing the input and arguments so easily, and this
syntax works in part because it still parenthesizes the `-1` literal value,
forcing `LitExpr` parsing, much like the new `ExprPath` syntax.
```graphql
# Produces [2, 4, 6] by doubling each field value
doubled: $(array.field)->map(@->mul(2))

# Produces [[2], [4], [6]], since ->map applied to a non-array produces a
# single-element array wrapping the result of the mapping expression applied
# to that individual value
nested: array.field->map(@->mul(2))
```

When you don't need to apply a `.key` or `->method` to a literal value within a
`LitExpr`, you do not need to wrap it with `$(...)`, so the `ExprPath` syntax is
relatively uncommon within `LitExpr` expressions.
In this capacity, the `$(...)` syntax is useful for controlling
associativity/grouping/precedence, similar to parenthesized expressions in other
programming languages.

### `PathStep ::=`

Expand Down Expand Up @@ -833,6 +855,55 @@ Also, as a minor syntactic convenience, `LitObject` literals can have
`Identifier` or `LitString` keys, whereas JSON objects can have only
double-quoted string literal keys.

### `LitPath ::=`

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

A `LitPath` is a special form of `PathSelection` (similar to `VarPath`,
`KeyPath`, `AtPath`, and `ExprPath`) that can be used _only_ within `LitExpr`
expressions, allowing the head of the path to be any `LitExpr` value, with a
non-empty tail of `PathStep` items afterward:

```graphql
object: $({
sd: "asdf"->slice(1, 3),
sum: 1234->add(5678),
celsius: 98.6->sub(32)->mul(5)->div(9),
nine: -1->add(10),
false: true->not,
true: false->not,
twenty: { a: 1, b: 2 }.b->mul(10),
last: [1, 2, 3]->last,
justA: "abc"->first,
justC: "abc"->last,
})
```

Note that expressions like `true->not` and `"asdf"->slice(1, 3)` have a
different interpretation in the default selection syntax (outside of `LitExpr`
parsing), since `true` and `"asdf"` will be interpreted as field names there,
not as literal values. If you want to refer to a quoted field value within a
`LitExpr`, you can use the `$.` variable prefix to disambiguate it:

```graphql
object: $({
fieldEntries: $."quoted field"->entries,
stringPrefix: "quoted field"->slice(0, "quoted"->size),
})
```

You can still nest the `$(...)` inside itself (or use it within `->` method
arguments), as in

```graphql
justA: $($("abc")->first)
nineAgain: $($(-1)->add($(10)))
```

In these examples, only the outermost `$(...)` wrapper is required, though the
inner wrappers may be used to clarify the structure of the expression, similar
to parentheses in other languages.

### `LitPrimitive ::=`

![LitPrimitive](./grammar/LitPrimitive.svg)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,9 @@ impl ApplyToInternal for WithRange<LitExpr> {
(Some(JSON::Array(output)), errors)
}
LitExpr::Path(path) => path.apply_to_path(data, vars, input_path),
LitExpr::LitPath(literal, subpath) => literal
.apply_to_path(data, vars, input_path)
.and_then_collecting_errors(|value| subpath.apply_to_path(value, vars, input_path)),
}
}

Expand Down Expand Up @@ -820,6 +823,21 @@ impl ApplyToInternal for WithRange<LitExpr> {
LitExpr::Path(path) => {
path.compute_output_shape(input_shape, dollar_shape, named_var_shapes, source_id)
}

LitExpr::LitPath(literal, subpath) => {
let literal_shape = literal.compute_output_shape(
input_shape.clone(),
dollar_shape.clone(),
named_var_shapes,
source_id,
);
subpath.compute_output_shape(
literal_shape,
dollar_shape,
named_var_shapes,
source_id,
)
}
}
}
}
Expand Down Expand Up @@ -2536,6 +2554,83 @@ mod tests {
);
}

#[test]
fn test_lit_paths() {
let data = json!({
"value": {
"key": 123,
},
});

assert_eq!(
selection!("$(\"a\")->first").apply_to(&data),
(Some(json!("a")), vec![]),
);

assert_eq!(
selection!("$('asdf'->last)").apply_to(&data),
(Some(json!("f")), vec![]),
);

assert_eq!(
selection!("$(1234)->add(1111)").apply_to(&data),
(Some(json!(2345)), vec![]),
);

assert_eq!(
selection!("$(1234->add(1111))").apply_to(&data),
(Some(json!(2345)), vec![]),
);

assert_eq!(
selection!("$(value.key->mul(10))").apply_to(&data),
(Some(json!(1230)), vec![]),
);

assert_eq!(
selection!("$(value.key)->mul(10)").apply_to(&data),
(Some(json!(1230)), vec![]),
);

assert_eq!(
selection!("$(value.key->typeof)").apply_to(&data),
(Some(json!("number")), vec![]),
);

assert_eq!(
selection!("$(value.key)->typeof").apply_to(&data),
(Some(json!("number")), vec![]),
);

assert_eq!(
selection!("$([1, 2, 3])->last").apply_to(&data),
(Some(json!(3)), vec![]),
);

assert_eq!(
selection!("$([1, 2, 3]->first)").apply_to(&data),
(Some(json!(1)), vec![]),
);

assert_eq!(
selection!("$({ a: 'ay', b: 1 }).a").apply_to(&data),
(Some(json!("ay")), vec![]),
);

assert_eq!(
selection!("$({ a: 'ay', b: 2 }.a)").apply_to(&data),
(Some(json!("ay")), vec![]),
);

assert_eq!(
// Note that the -> has lower precedence than the -, so -1 is parsed
// as a completed expression before applying the ->add(10) method,
// giving 9 instead of -11.
selection!("$(-1->add(10))").apply_to(&data),
(Some(json!(9)), vec![]),
);
}

#[test]
fn test_compute_output_shape() {
assert_eq!(selection!("").shape().pretty_print(), "{}");
Expand Down
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