Add painless method getByPath, get value from nested collections with dotted path#43170
Conversation
… dotted path
Given a nested structure composed of Lists and Maps, getByPath will return the value
keyed by path. getByPath is a method on Lists and Maps.
The path is string Map keys and integer List indices separated by dot. An optional third
argument returns a default value if the path lookup fails due to a missing value.
Eg.
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1') = ['c', 'd']
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1.0') = 'c'
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key2', 'x') = 'x'
[['key0': 'value0'], ['key1': 'value1']].getByPath('1.key1') = 'value1'
Throws IllegalArgumentException if an item cannot be found and a default is not given.
Throws NumberFormatException if a path element operating on a List is not an integer.
Fixes elastic#42769
|
Pinging @elastic/es-core-infra |
rjernst
left a comment
There was a problem hiding this comment.
I left a few comments. Overall I think we can make this cleaner by using recursion in this case, since we need to switch back and forth between types. You could still do this iteratively, but then I would have the first part of the loop do the extraction of the next element, and the second part do the dispatch of the next receiver. The downside is we would still have an extra instanceof check when we already know the receiver type from the whitelisted methods.
I also am wondering if we should be more strict in the default value behavior, but still allow for missing behavior. My suggestion would be to make this work strictly using the current dot syntax, and in a followup add support in the path syntax for the null-safe operator. This way a user can be very explicit if they want lenient behavior at any element within the path.
modules/lang-painless/src/test/java/org/elasticsearch/painless/AugmentationTests.java
Outdated
Show resolved
Hide resolved
modules/lang-painless/src/test/java/org/elasticsearch/painless/AugmentationTests.java
Show resolved
Hide resolved
modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Augmentation.java
Outdated
Show resolved
Hide resolved
modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Augmentation.java
Outdated
Show resolved
Hide resolved
modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Augmentation.java
Outdated
Show resolved
Hide resolved
modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Augmentation.java
Outdated
Show resolved
Hide resolved
Separate getByPath tests into different test methods rather than a single test driven by cases. Raise IllegalArgumentException when object at non-terminal path element is not a container, even if defaultValue is given. Check for double . in paths and paths ending in .
rjernst
left a comment
There was a problem hiding this comment.
This looks good, just a few more minor comments.
modules/lang-painless/src/test/java/org/elasticsearch/painless/GetByPathAugmentationTests.java
Outdated
Show resolved
Hide resolved
modules/lang-painless/src/test/java/org/elasticsearch/painless/GetByPathAugmentationTests.java
Outdated
Show resolved
Hide resolved
| * Throws 'IllegalArgumentException' if elements[i] is an empty string, indicating double separators | ||
| * or path ends in a separator. | ||
| */ | ||
| private static Object getByPath(Object obj, String[] elements, int i, Supplier<Object> defaultSupplier) { |
There was a problem hiding this comment.
I think it would be more clear with each of these names having one more word. Eg Dispatch here, Map/List below.
modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Augmentation.java
Outdated
Show resolved
Hide resolved
modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Augmentation.java
Outdated
Show resolved
Hide resolved
modules/lang-painless/src/main/java/org/elasticsearch/painless/api/Augmentation.java
Outdated
Show resolved
Hide resolved
Rename helper functions. Use javadoc for public functions. Optimize empty path check.
modules/lang-painless/src/test/java/org/elasticsearch/painless/GetByPathAugmentationTests.java
Outdated
Show resolved
Hide resolved
jdconrad
left a comment
There was a problem hiding this comment.
LGTM. Thanks for adding this method Stuart!
… dotted path (elastic#43170) Given a nested structure composed of Lists and Maps, getByPath will return the value keyed by path. getByPath is a method on Lists and Maps. The path is string Map keys and integer List indices separated by dot. An optional third argument returns a default value if the path lookup fails due to a missing value. Eg. ['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1') = ['c', 'd'] ['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1.0') = 'c' ['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key2', 'x') = 'x' [['key0': 'value0'], ['key1': 'value1']].getByPath('1.key1') = 'value1' Throws IllegalArgumentException if an item cannot be found and a default is not given. Throws NumberFormatException if a path element operating on a List is not an integer. Fixes elastic#42769
… dotted path (#43170) (#43606) Given a nested structure composed of Lists and Maps, getByPath will return the value keyed by path. getByPath is a method on Lists and Maps. The path is string Map keys and integer List indices separated by dot. An optional third argument returns a default value if the path lookup fails due to a missing value. Eg. ['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1') = ['c', 'd'] ['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1.0') = 'c' ['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key2', 'x') = 'x' [['key0': 'value0'], ['key1': 'value1']].getByPath('1.key1') = 'value1' Throws IllegalArgumentException if an item cannot be found and a default is not given. Throws NumberFormatException if a path element operating on a List is not an integer. Fixes #42769
Given a nested structure composed of Lists and Maps, getByPath will return the value
keyed by path. getByPath is a method on Lists and Maps.
The path is string Map keys and integer List indices separated by dot. An optional third
argument returns a default value if the path lookup fails due to a missing value.
Eg.
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1') = ['c', 'd']
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key1.0') = 'c'
['key0': ['a', 'b'], 'key1': ['c', 'd']].getByPath('key2', 'x') = 'x'
[['key0': 'value0'], ['key1': 'value1']].getByPath('1.key1') = 'value1'
Throws IllegalArgumentException if an item cannot be found and a default is not given.
Throws NumberFormatException if a path element operating on a List is not an integer.
Fixes #42769