jmespath-ruby 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +18 -0
  3. data/CONTRIBUTING.md +57 -0
  4. data/LICENSE +21 -0
  5. data/LICENSES/python-MIT.txt +21 -0
  6. data/LICENSES/ruby-Apache-2.0.txt +174 -0
  7. data/README.md +122 -0
  8. data/SECURITY.md +16 -0
  9. data/VERSIONING.md +12 -0
  10. data/docs/guides/compatibility.md +84 -0
  11. data/docs/guides/compilation.md +63 -0
  12. data/docs/guides/errors.md +30 -0
  13. data/docs/guides/functions.md +65 -0
  14. data/docs/guides/performance.md +237 -0
  15. data/docs/guides/search.md +56 -0
  16. data/exe/jmespath +5 -0
  17. data/exe/jmespath.rb +5 -0
  18. data/lib/jmespath/ast.rb +43 -0
  19. data/lib/jmespath/cache.rb +53 -0
  20. data/lib/jmespath/cli.rb +36 -0
  21. data/lib/jmespath/data_access.rb +66 -0
  22. data/lib/jmespath/errors.rb +53 -0
  23. data/lib/jmespath/evaluation/construction.rb +20 -0
  24. data/lib/jmespath/evaluation/logic.rb +42 -0
  25. data/lib/jmespath/evaluation/projections.rb +43 -0
  26. data/lib/jmespath/evaluation/scalars.rb +49 -0
  27. data/lib/jmespath/evaluation/slices.rb +50 -0
  28. data/lib/jmespath/evaluator.rb +37 -0
  29. data/lib/jmespath/expression.rb +63 -0
  30. data/lib/jmespath/expression_reference.rb +15 -0
  31. data/lib/jmespath/functions/coercions.rb +41 -0
  32. data/lib/jmespath/functions/collections.rb +84 -0
  33. data/lib/jmespath/functions/conversion.rb +32 -0
  34. data/lib/jmespath/functions/function_call.rb +32 -0
  35. data/lib/jmespath/functions/references.rb +62 -0
  36. data/lib/jmespath/functions/scalar.rb +74 -0
  37. data/lib/jmespath/functions/signature.rb +77 -0
  38. data/lib/jmespath/functions.rb +73 -0
  39. data/lib/jmespath/input.rb +19 -0
  40. data/lib/jmespath/lexer.rb +132 -0
  41. data/lib/jmespath/normalizer.rb +53 -0
  42. data/lib/jmespath/options.rb +67 -0
  43. data/lib/jmespath/parser/expressions.rb +70 -0
  44. data/lib/jmespath/parser/projections.rb +91 -0
  45. data/lib/jmespath/parser.rb +119 -0
  46. data/lib/jmespath/plan.rb +153 -0
  47. data/lib/jmespath/token.rb +6 -0
  48. data/lib/jmespath/types.rb +84 -0
  49. data/lib/jmespath/version.rb +7 -0
  50. data/lib/jmespath.rb +100 -0
  51. data/sig/jmespath.rbs +55 -0
  52. metadata +135 -0
@@ -0,0 +1,65 @@
1
+ # Functions
2
+
3
+ Function arguments are evaluated before type validation.
4
+ Expression references, written as `&expression`, defer evaluation until a function supplies an input.
5
+
6
+ ## Standard functions
7
+
8
+ | Function | Accepted arguments | Result |
9
+ | --- | --- | --- |
10
+ | `abs` | number | Absolute value |
11
+ | `avg` | array of numbers | Mean, or nil for an empty array |
12
+ | `ceil` | number | Integer rounded upward |
13
+ | `floor` | number | Integer rounded downward |
14
+ | `contains` | string or array, value | Membership boolean |
15
+ | `ends_with` | string, string | Suffix boolean |
16
+ | `starts_with` | string, string | Prefix boolean |
17
+ | `length` | string, array, or object | Character or element count |
18
+ | `reverse` | string or array | Reversed value |
19
+ | `join` | string separator, array of strings | Joined string |
20
+ | `keys` | object | String keys |
21
+ | `values` | object | Values |
22
+ | `merge` | one or more objects | Object with later values winning |
23
+ | `map` | expression reference, array | Mapped values, including nil |
24
+ | `min` | homogeneous array of numbers or strings | Minimum, or nil |
25
+ | `max` | homogeneous array of numbers or strings | Maximum, or nil |
26
+ | `sort` | homogeneous array of numbers or strings | Stable sorted array |
27
+ | `sort_by` | array, expression reference | Stable array sorted by homogeneous keys |
28
+ | `min_by` | array, expression reference | Input element with minimum key, or nil |
29
+ | `max_by` | array, expression reference | Input element with maximum key, or nil |
30
+ | `sum` | array of numbers | Sum, including zero for an empty array |
31
+ | `not_null` | one or more values | First non-nil value |
32
+ | `to_array` | value | Array, preserving arrays |
33
+ | `to_number` | value | Number when convertible, otherwise nil |
34
+ | `to_string` | value | String or compact JSON representation |
35
+ | `type` | value | JMESPath type name |
36
+
37
+ ```ruby
38
+ JMESPath.search("map(&name, @)", [{name: "Ada"}, {}]) #=> ["Ada", nil]
39
+ JMESPath.search("sort_by(@, &age)[*].name", [{name: "Lin", age: 20}, {name: "Ada", age: 10}]) #=> ["Ada", "Lin"]
40
+ JMESPath.search("avg(@)", [1, 2, 3]) #=> 2.0
41
+ ```
42
+
43
+ ## Custom signatures
44
+
45
+ Start with `Functions.default` to retain the built-ins, or `Functions.new` for an empty registry.
46
+ Use an array of accepted type symbols for each argument.
47
+ Type symbols are `:any`, `:null`, `:boolean`, `:number`, `:string`, `:array`, `:object`, and `:expref`.
48
+ Typed arrays use `:array_number` or `:array_string`.
49
+ A union of those array types accepts either homogeneous array, including an empty array.
50
+
51
+ ```ruby
52
+ functions = JMESPath::Functions.default.with("size_or_zero", signature: [[:string, :null]]) do |value|
53
+ value.nil? ? 0 : value.length
54
+ end
55
+ JMESPath.search("size_or_zero(missing)", {}, functions: functions) #=> 0
56
+ ```
57
+
58
+ With `variadic: true`, the final argument signature repeats and remains required.
59
+ Every supplied argument is checked.
60
+ Duplicate names require `override: true`.
61
+ Registration returns a new frozen registry and leaves the original intact.
62
+
63
+ Callbacks receive evaluated Ruby values and may retain selected object identity.
64
+ Return a supported value and avoid mutating shared input.
65
+ Callback exceptions propagate with their original object and backtrace.
@@ -0,0 +1,237 @@
1
+ # Performance
2
+
3
+ Compile an expression explicitly when evaluating it against many documents.
4
+ The convenience cache avoids parsing repeated queries, while explicit compilation also avoids cache lookup.
5
+
6
+ ```ruby
7
+ expression = JMESPath.compile("items[*].price | sum(@)")
8
+ expression.search({items: [{price: 10}, {price: 20}]}) #=> 30
9
+ ```
10
+
11
+ ## Reproduce
12
+
13
+ Prepare a checkout of [jmespath.rb at commit 5e86ea7](https://github.com/jmespath/jmespath.rb/tree/5e86ea7da2e954cc12f5b3a8383a43c0a65acba7).
14
+ The harness performs no downloads or installs.
15
+
16
+ ```sh
17
+ bundle exec ruby script/benchmark.rb --ruby-root /path/to/jmespath.rb --iterations 2000 --rounds 5 --memory --output /tmp/jmespath-benchmark.json
18
+ ```
19
+
20
+ The default suite includes all 12 cases from the four upstream benchmark files and six supplemental workloads.
21
+ Upstream files remain unchanged; `benchmark/upstream/sources.json` records their commit, license, and SHA-256 hashes.
22
+ Each workload runs in startup, repeated search, cached runtime reuse, direct compiled reuse, and cache-churn modes.
23
+ Use `--only field` to select one workload.
24
+
25
+ Cached runtime reuse compares `Expression#search` with the reference's persistent `Runtime#search`, following its `benchmark:cached` task.
26
+ Direct compiled reuse compares `Expression#search` with the reference's optimized node, bypassing its runtime.
27
+
28
+ The `--memory` option requires macOS `/usr/bin/time -l` and `ps`.
29
+ Omit that option to measure timing and allocations without the macOS memory tools.
30
+ Startup timing with `--memory` includes memory reporting; omit it when measuring cold latency alone.
31
+
32
+ ## Measurements
33
+
34
+ These measurements compare revision `367f256e81f31ebff698466b768da0164610dd03` with the pinned jmespath.rb reference on arm64 macOS.
35
+ Each run uses default GC settings, disabled YJIT, and no Bundler.
36
+ Each sample performs 320 warmup evaluations and 2,000 measured evaluations, except startup samples.
37
+ Three paired samples alternate implementation order in separate processes using the same Ruby interpreter.
38
+
39
+ All 18 workloads have lower median times in repeated search, cached runtime reuse, and cache churn on each tested Ruby version.
40
+ Speedup is reference time divided by library time; larger values indicate faster execution.
41
+ Peak RAM includes process startup and warmup.
42
+
43
+ | MRI | Repeated search | Cached runtime reuse | Cache churn | Largest peak RAM increase across these modes |
44
+ | --- | ---: | ---: | ---: | ---: |
45
+ | 3.3.11 | 1.40x to 86.51x | 1.10x to 79.11x | 1.19x to 2.48x | 2.36 MiB (12.3%) |
46
+ | 3.4.9 | 1.38x to 85.23x | 1.11x to 76.47x | 1.19x to 2.48x | 3.52 MiB (17.4%) |
47
+ | 4.0.9 | 1.35x to 74.36x | 1.04x to 70.93x | 1.16x to 2.27x | 1.25 MiB (5.4%) |
48
+
49
+ Cached runtime reuse follows the upstream cached benchmark: this library reuses an `Expression`, and the reference reuses a `Runtime`.
50
+ The implementations return the same checked results, including the upstream cases and six supplemental workloads.
51
+
52
+ Raw samples, runtime metadata, and source fingerprints are available for
53
+ [MRI 3.3.11](../../benchmark/results/mri-3.3.11-speed.json),
54
+ [MRI 3.4.9](../../benchmark/results/mri-3.4.9-speed.json), and
55
+ [MRI 4.0.9](../../benchmark/results/mri-4.0.9-speed.json).
56
+
57
+ Longer checks use 20,000 evaluations, five paired samples, and no memory collection.
58
+ Cached `wildcard_with_field_match2` is 1.10x faster on MRI 4.0.9; cached `multi_wildcard_field` is 1.44x faster on MRI 3.3.11.
59
+ The [follow-up samples](../../benchmark/results/cached-margin-checks.json) check the smallest cached speed margin and a noisy sample from the full runs.
60
+
61
+ ### Saved query options
62
+
63
+ Revision `bb075c70dd86cd52d6b91c6ab2acb0f9f6e916a6` adds saved evaluation options to compiled queries.
64
+ These measurements compare saved options with per-call options on that revision and on the preceding implementation, `f73c7e4`.
65
+ Each workload uses five alternating samples, 640 warmup searches, and 10,000 measured searches in separate processes.
66
+ All runs use default GC settings, disabled YJIT, no Bundler, and arm64 macOS.
67
+ Results are checked before and after timing.
68
+
69
+ | MRI | Speedup over preceding per-call options | Speedup over same-revision per-call options | Largest peak RAM increase over preceding version |
70
+ | --- | ---: | ---: | ---: |
71
+ | 3.3.11 | 1.57x to 7.49x | 1.59x to 7.85x | 2.48 MiB |
72
+ | 3.4.9 | 1.55x to 8.25x | 1.58x to 8.87x | 7.08 MiB |
73
+ | 4.0.9 | 1.49x to 6.41x | 1.49x to 6.73x | 2.08 MiB |
74
+
75
+ The seven cases cover field traversal, projection, a custom function, an unchanged built-in, a built-in override, and valid and invalid suppressed calls.
76
+ They use the `items` document in `benchmark/documents.json` and these configurations:
77
+
78
+ | Case | Expression | Saved options |
79
+ | --- | --- | --- |
80
+ | Field | `meta.owner.name` | Registry extended with `double` |
81
+ | Projection | `items[*].price` | Registry extended with `double` |
82
+ | Custom | `double(items[3].price)` | Registry extended with `double` |
83
+ | Unchanged built-in | `length(text)` | Registry extended with `double` |
84
+ | Override | `length(text)` | Override `length` to return twice the string length |
85
+ | Suppressed | `length(text)` | `disable_visit_errors: true` |
86
+ | Invalid | `length(items[0].active)` | `disable_visit_errors: true` |
87
+
88
+ The `double` function accepts one number and returns twice its value; the override accepts one string.
89
+ On MRI 4.0, field lookup drops from six allocated objects per search in the preceding version to one with saved options.
90
+ Peak RAM includes process startup and warmup; it is measured with macOS `/usr/bin/time -l`.
91
+ The largest increase is projection on MRI 3.4: 27.66 MiB versus 20.58 MiB.
92
+ Saving options reduces temporary allocations, but the default GC schedule can retain more array memory between collections.
93
+
94
+ Default search controls cover all 18 existing workloads.
95
+ Their median speed ratios range from 0.99x to 1.09x for public search and 0.97x to 1.10x for compiled search, excluding one short-run outlier.
96
+ That MRI 3.3 deep-query outlier initially measured 0.94x; seven paired 100,000-search samples measured 1.001x.
97
+ This does not establish a regression in that workload.
98
+
99
+ Raw samples and source fingerprints: [MRI 3.3.11](../../benchmark/results/configured-mri-3.3.11.json),
100
+ [MRI 3.4.9](../../benchmark/results/configured-mri-3.4.9.json), [MRI 4.0.9](../../benchmark/results/configured-mri-4.0.9.json),
101
+ and the [deep-query recheck](../../benchmark/results/configured-deep-recheck.json).
102
+
103
+ ### Direct-node diagnostics
104
+
105
+ The `compiled` mode calls the reference's private optimized node directly, bypassing its runtime API.
106
+ This library still calls `Expression#search`, which supports per-call options and input adapters.
107
+ This stricter comparison does not show a speedup for every workload:
108
+
109
+ | MRI | Workloads faster | Workloads without a median speedup |
110
+ | --- | ---: | --- |
111
+ | 3.3.11 | 17 of 18 | `single_expression` |
112
+ | 3.4.9 | 17 of 18 | `single_expression` |
113
+ | 4.0.9 | 15 of 18 | `single_expression`, `wildcard_with_field_match`, `wildcard_with_field_match2` |
114
+
115
+ Startup samples include memory-reporting overhead and do not establish pure load-time improvements.
116
+
117
+ ## Earlier measurements
118
+
119
+ These workload-specific measurements compare revision `2d061978319b7757443e52c5aba01992ab8c34b0` with jmespath.rb 1.6.2 on MRI 4.0.9, arm64 macOS, with YJIT disabled.
120
+ Each sample performs 320 warmup evaluations and 2,000 measured evaluations, except startup samples, which perform one evaluation without warmup.
121
+ Five paired samples alternate implementation order.
122
+ Implementations run in separate processes using the same Ruby interpreter and Bundler environment.
123
+ Expected results are checked before and after timing; startup checks its result after timing.
124
+ The harness rejects source or fixture changes during a run.
125
+
126
+ Raw samples, runtime details, and source fingerprints are in [mri-4.0.9-optimized.json](../../benchmark/results/mri-4.0.9-optimized.json).
127
+ The earlier, unoptimized baseline remains in [mri-4.0.9.json](../../benchmark/results/mri-4.0.9.json).
128
+
129
+ Search speedup is reference time divided by library time; values above 1 indicate faster execution.
130
+ Tables show median allocations per 2,000 searches and whole-process peak resident memory, including startup and warmup.
131
+ Memory collection occurs outside the timed search loop.
132
+ Retained Ruby object bytes are also recorded after full garbage collection; they exclude some native allocations and are not process RAM.
133
+ Allocation counts alone do not establish lower RAM usage.
134
+
135
+ 18 of 18 workloads were faster; 17 had equal or lower median peak RAM.
136
+
137
+ ### Upstream workloads
138
+
139
+ | Workload | Search speedup | Peak RAM, library / reference (MiB) | Allocations, library / reference |
140
+ | --- | ---: | ---: | ---: |
141
+ | single_expression | 2.70x | 29.12 / 29.88 | 6007 / 46011 |
142
+ | single_dot_expression | 4.81x | 28.97 / 29.64 | 6007 / 102015 |
143
+ | double_dot_expression | 6.98x | 29.02 / 29.67 | 6007 / 148017 |
144
+ | dot_no_match | 7.97x | 29.02 / 29.86 | 6007 / 196023 |
145
+ | deep_nesting_10 | 17.10x | 29.08 / 30.02 | 6007 / 526029 |
146
+ | deep_nesting_50 | 57.57x | 29.30 / 30.78 | 6007 / 4546109 |
147
+ | deep_nesting_50_pipe | 56.31x | 29.39 / 31.00 | 6007 / 4546109 |
148
+ | deep_nesting_50_index | 28.36x | 29.25 / 30.11 | 6007 / 4950009 |
149
+ | multi_wildcard_field | 4.59x | 29.16 / 29.92 | 12007 / 166041 |
150
+ | wildcard_with_index | 5.81x | 29.03 / 29.91 | 8007 / 222051 |
151
+ | wildcard_with_field_match | 3.83x | 29.19 / 30.08 | 8007 / 104020 |
152
+ | wildcard_with_field_match2 | 4.18x | 29.03 / 29.98 | 8007 / 110028 |
153
+
154
+ ### Supplemental workloads
155
+
156
+ | Workload | Search speedup | Peak RAM, library / reference (MiB) | Allocations, library / reference |
157
+ | --- | ---: | ---: | ---: |
158
+ | field | 7.29x | 28.84 / 29.72 | 6007 / 156016 |
159
+ | projection | 1.61x | 30.80 / 30.23 | 8007 / 112032 |
160
+ | filter | 1.44x | 30.05 / 30.39 | 8007 / 126016 |
161
+ | sort | 1.18x | 29.75 / 29.88 | 226007 / 460012 |
162
+ | slice | 3.55x | 29.50 / 29.83 | 14007 / 174018 |
163
+ | unicode | 4.80x | 29.31 / 29.84 | 22007 / 204015 |
164
+
165
+ ### Compiled reuse and cache churn
166
+
167
+ Compiled reuse: 2 of 18 workloads were faster; 17 had equal or lower median peak RAM.
168
+ Cache churn: 18 of 18 workloads were faster; 11 had equal or lower median peak RAM.
169
+
170
+ The reference compiled baseline visits its optimized tree through a private API because it has no public `compile` equivalent.
171
+ `Expression#search` also supports per-call options and input adapters.
172
+ These operations are reported separately from the public `JMESPath.search` comparison.
173
+
174
+ Churn cycles through 320 distinct expressions, exceeding the library's 32-entry cache.
175
+ The reference constructs a runtime and parser for each public search, so its cache does not persist between those calls.
176
+ This mode tests the same query stream against each implementation's public behavior.
177
+
178
+ ### Default GC across supported MRI series
179
+
180
+ The earlier implementation did not meet strict peak-RAM parity on MRI 3.3, 3.4, and 4.0.
181
+ Public search is faster in all 18 workloads on each tested version, but some workloads use more peak RAM.
182
+ The implementation remains pure Ruby; these comparisons exclude application-level GC tuning.
183
+
184
+ Additional runs use MRI 3.3.11 and 3.4.9 without Bundler, with default GC settings and YJIT disabled.
185
+ Each run uses the same library source, 320 warmup evaluations, 2,000 measured evaluations, and three alternating paired samples.
186
+ Compare implementations within each run; the MRI 4.0.9 results above use a different process environment and sample count.
187
+
188
+ | MRI | Search speedup range | Search RAM target met | Compiled workloads faster | Churn RAM target met |
189
+ | --- | ---: | ---: | ---: | ---: |
190
+ | 3.3.11 | 1.14x to 61.58x | 14 of 18 | 7 of 18 | 15 of 18 |
191
+ | 3.4.9 | 1.21x to 60.33x | 17 of 18 | 6 of 18 | 5 of 18 |
192
+
193
+ Churn is faster in all 18 workloads on both versions.
194
+ Raw results include every mode and sample: [MRI 3.3.11](../../benchmark/results/mri-3.3.11-defaults.json) and [MRI 3.4.9](../../benchmark/results/mri-3.4.9-defaults.json).
195
+ Repeat either run with its installed Ruby interpreter:
196
+
197
+ ```sh
198
+ ruby script/benchmark.rb --ruby-root /path/to/jmespath.rb --iterations 2000 --rounds 3 --memory --output /tmp/jmespath-defaults.json
199
+ ```
200
+
201
+ A separate projection diagnostic uses 640 warmup evaluations, 20,000 measured searches, and three paired samples without Bundler.
202
+ The table reports median peak RAM and collections during the measured loop.
203
+
204
+ | MRI | Search speedup | Peak RAM, library / reference (MiB) | GC collections, library / reference |
205
+ | --- | ---: | ---: | ---: |
206
+ | 3.3.11 | 1.72x | 19.28 / 18.36 | 12 / 173 |
207
+ | 3.4.9 | 1.70x | 20.97 / 19.50 | 8 / 95 |
208
+ | 4.0.9 | 1.61x | 22.44 / 21.14 | 10 / 96 |
209
+
210
+ Retained Ruby object bytes are lower in all three diagnostic runs, while peak RAM is 5% to 8% higher.
211
+ Native snapshots on MRI 4.0.9 show array memory returning to baseline after full GC, while about 1 MiB of extra physical footprint remains.
212
+ This supports temporary array accumulation and native allocator retention as causes of the gap in that workload.
213
+
214
+ Three isolated alternatives failed to meet both targets.
215
+ Growing projection arrays incrementally leaves peak RAM 7% to 9% higher and reduces speed.
216
+ An eight-entry cache leaves peak RAM 14% higher in the deep-query churn case on MRI 3.4.9.
217
+ Uncached public search makes the projection case 5% slower than the reference, with peak RAM 0.4% higher.
218
+ These experimental alternatives were not adopted.
219
+ The [diagnostic report](../../benchmark/results/default-gc-diagnosis.json) preserves raw samples, source fingerprints, and native memory snapshots.
220
+
221
+ ## Optimization decisions
222
+
223
+ Default searches reuse immutable configuration and compiled operations.
224
+ Field readers share the data-access implementation.
225
+ Slices prepare their bounds and direction once per compiled expression.
226
+ Native string length and reversal bypass generic function dispatch.
227
+ Function arguments and expression-reference callbacks use compiled operations; function signatures remain checked.
228
+ Operations without a compiled handler use the evaluator.
229
+ Compiled expressions can save a custom registry and error policy while retaining compiled operations.
230
+ Field dispatch avoids the generic handler lookup.
231
+ Projections compact a single result array, and stable sorting uses one key-and-index pair per item.
232
+ Whitespace runs are scanned together.
233
+ The cache retains 32 expressions, with the existing 1,048,576-byte source limit.
234
+
235
+ An experimental cache removed default-hit allocations but increased peak RAM by 32% in a 20,000-search projection comparison.
236
+ That variant was rejected.
237
+ Ruby's garbage-collection schedule can make fewer allocations coexist with higher peak RAM; compare both metrics on representative workloads.
@@ -0,0 +1,56 @@
1
+ # Search semantics
2
+
3
+ JMESPath expressions select and transform structured values.
4
+ The [language specification](https://jmespath.org/specification.html) defines the grammar and standard functions.
5
+
6
+ ## Fields and indices
7
+
8
+ ```ruby
9
+ JMESPath.search("user.name", {user: {name: "Ada"}}) #=> "Ada"
10
+ JMESPath.search("items[-1]", {items: [10, 20, 30]}) #=> 30
11
+ JMESPath.search("items[99]", {items: [10]}) #=> nil
12
+ ```
13
+
14
+ Hash fields accept string and existing symbol keys.
15
+ A non-nil string value wins when both forms exist.
16
+ A nil string value allows fallback to the symbol key.
17
+ Struct lookup visits members, not arbitrary methods.
18
+
19
+ ## Projections and flattening
20
+
21
+ A projection applies its right expression to each input element.
22
+ Nil results are omitted; false results remain present.
23
+
24
+ ```ruby
25
+ JMESPath.search("[*].value", [{value: 1}, {}, {value: false}]) #=> [1, false]
26
+ JMESPath.search("[]", [[1, 2], [3, [4]]]) #=> [1, 2, 3, [4]]
27
+ JMESPath.search("[*].value | [0]", [{value: 1}, {value: 2}]) #=> 1
28
+ ```
29
+
30
+ Flattening removes one array level.
31
+ A pipe evaluates its right expression against the entire left result, ending projection propagation.
32
+
33
+ ## Filters and truthiness
34
+
35
+ False, nil, empty strings, empty arrays, and empty objects are false values.
36
+ Zero is true in JMESPath.
37
+
38
+ ```ruby
39
+ JMESPath.search("items[?enabled].id", {items: [{id: 1, enabled: true}, {id: 2, enabled: false}]}) #=> [1]
40
+ JMESPath.search("a || b", {a: [], b: "fallback"}) #=> "fallback"
41
+ JMESPath.search("!value", {value: 0}) #=> false
42
+ ```
43
+
44
+ Boolean operators short-circuit and return the selected operand.
45
+
46
+ ## Slices and constructed values
47
+
48
+ ```ruby
49
+ JMESPath.search("[::-1]", [1, 2, 3]) #=> [3, 2, 1]
50
+ JMESPath.search("[:0:-1]", [1, 2, 3]) #=> [3, 2]
51
+ JMESPath.search("{name: user.name, missing: absent}", {user: {name: "Ada"}}) #=> {"name" => "Ada", "missing" => nil}
52
+ ```
53
+
54
+ An omitted negative-slice stop differs from an explicit `-1`.
55
+ A zero slice step raises InvalidValueError.
56
+ Multiselects construct new arrays or hashes and retain explicit nil values.
data/exe/jmespath ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/jmespath/cli"
5
+ exit JMESPath::CLI.run(ARGV)
data/exe/jmespath.rb ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require_relative "../lib/jmespath/cli"
5
+ exit JMESPath::CLI.run(ARGV)
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ module AST
6
+ # Immutable syntax and literal templates.
7
+ class Node
8
+ attr_reader :kind, :value, :children, :start, :finish, :depth
9
+
10
+ def initialize(kind:, value: nil, children: [], start: 0, finish: 0)
11
+ @kind = kind
12
+ @value = self.class.immutable(value)
13
+ @children = children.freeze
14
+ @start = start
15
+ @finish = finish
16
+ @depth = if kind == :literal
17
+ self.class.literal_depth(value)
18
+ else
19
+ (children.map(&:depth).max || -1) + 1
20
+ end
21
+ freeze
22
+ end
23
+
24
+ def self.literal_depth(value)
25
+ values = case value
26
+ when Array then value
27
+ when Hash then value.values
28
+ else return 0
29
+ end
30
+ 1 + (values.map { |entry| literal_depth(entry) }.max || 0)
31
+ end
32
+
33
+ def self.immutable(value)
34
+ case value
35
+ when Array then value.map { |item| immutable(item) }.freeze
36
+ when Hash then value.transform_values { |item| immutable(item) }.freeze
37
+ when String then value.dup.freeze
38
+ else value
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Cache
6
+ def initialize(limit: 32, bytes: 1_048_576)
7
+ raise ArgumentError, "Cache capacities must be positive" unless limit.positive? && bytes.positive?
8
+
9
+ @limit = limit
10
+ @budget = bytes
11
+ @used = 0
12
+ @entries = {}
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def fetch(source, max_depth:)
17
+ return yield source if source.bytesize > @budget
18
+
19
+ # @type var key: [String, Integer]
20
+ key = [source.frozen? ? source : source.dup.freeze, max_depth].freeze
21
+ cached = @mutex.synchronize { read(key) }
22
+ return cached if cached
23
+
24
+ value = yield key.first
25
+ @mutex.synchronize { write(key, value) }
26
+ value
27
+ end
28
+
29
+ private
30
+
31
+ def read(key)
32
+ value = @entries.delete(key)
33
+ @entries[key] = value if value
34
+ value
35
+ end
36
+
37
+ def write(key, value)
38
+ @used -= key.first.bytesize if @entries.delete(key)
39
+ evict_for(key.first.bytesize)
40
+ @entries[key] = value
41
+ @used += key.first.bytesize
42
+ end
43
+
44
+ def evict_for(size)
45
+ @entries.each_key do |key|
46
+ break if @entries.length < @limit && @used + size <= @budget
47
+
48
+ @entries.delete(key)
49
+ @used -= key.first.bytesize
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../jmespath"
4
+
5
+ module JMESPath
6
+ # Runs one JMESPath query against a JSON document from standard input.
7
+ module CLI
8
+ USAGE = "Usage: jmespath EXPRESSION < document.json"
9
+
10
+ # @param argv [Array<String>] command-line arguments
11
+ # @param stdin [IO, StringIO] source JSON stream
12
+ # @param stdout [IO, StringIO] result stream
13
+ # @param stderr [IO, StringIO] diagnostic stream
14
+ # @return [Integer] zero on success, one on input failure, two on usage failure
15
+ def self.run(argv, stdin: $stdin, stdout: $stdout, stderr: $stderr)
16
+ return information(argv, stdout) if [%w[--help], %w[-h], %w[--version]].include?(argv)
17
+
18
+ unless argv.length == 1
19
+ stderr.puts(USAGE)
20
+ return 2
21
+ end
22
+ data = JSON.parse(stdin.read.to_s)
23
+ stdout.puts(JSON.generate(JMESPath.search(argv.fetch(0), data)))
24
+ 0
25
+ rescue JSON::ParserError, Errors::Error, IOError, SystemCallError => e
26
+ stderr.puts(e.message)
27
+ 1
28
+ end
29
+
30
+ # @api private
31
+ def self.information(argv, output)
32
+ output.puts(argv == ["--version"] ? "jmespath-ruby #{VERSION}" : USAGE)
33
+ 0
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ module DataAccess
6
+ # @api private
7
+ module FieldLookup
8
+ def field(value, name = @field_name)
9
+ mapping = value.is_a?(Hash) ? value : DataAccess.object(value)
10
+ return nil unless mapping
11
+
12
+ result = mapping[name]
13
+ return result unless result.nil?
14
+
15
+ mapping.each_key do |key|
16
+ return mapping[key] if key.is_a?(Symbol) && key.name == name
17
+ end
18
+ nil
19
+ end
20
+ end
21
+
22
+ extend FieldLookup
23
+
24
+ # @api private
25
+ class FieldReader
26
+ include FieldLookup
27
+
28
+ def initialize(name)
29
+ @field_name = name
30
+ freeze
31
+ end
32
+
33
+ alias call field
34
+
35
+ private :field
36
+ end
37
+
38
+ class << self
39
+ def array(value)
40
+ convert(value, Array, :to_ary)
41
+ end
42
+
43
+ def object(value)
44
+ return value.to_h if value.is_a?(Struct)
45
+
46
+ convert(value, Hash, :to_hash)
47
+ end
48
+
49
+ def string(value)
50
+ convert(value, String, :to_str)
51
+ end
52
+
53
+ private
54
+
55
+ def convert(value, type, method)
56
+ return value if value.is_a?(type)
57
+ return nil unless value.respond_to?(method)
58
+
59
+ result = value.public_send(method)
60
+ return result if result.nil? || result.is_a?(type)
61
+
62
+ raise TypeError, "#{method} must return #{type}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # Failures reported by the query language.
5
+ module Errors
6
+ # Base class for JMESPath failures.
7
+ class Error < StandardError; end
8
+ # A runtime evaluation failure.
9
+ class RuntimeError < Error; end
10
+
11
+ # A malformed expression with a byte position and readable coordinates.
12
+ class SyntaxError < Error
13
+ # @return [String] the expression text
14
+ attr_reader :expression
15
+ # @return [Integer] zero-based byte offset
16
+ attr_reader :position
17
+
18
+ # @param message [String] the reason parsing failed
19
+ # @param expression [String] expression text
20
+ # @param position [Integer] zero-based byte offset
21
+ def initialize(message, expression: "", position: 0)
22
+ @expression = expression.dup.freeze
23
+ @position = position
24
+ super("#{message} at byte #{position}")
25
+ end
26
+
27
+ # @return [Integer] one-based line number
28
+ def line
29
+ prefix.count("\n") + 1
30
+ end
31
+
32
+ # @return [Integer] one-based character column
33
+ def column
34
+ (prefix.split("\n", -1).last || "").length + 1
35
+ end
36
+
37
+ private
38
+
39
+ def prefix
40
+ expression.byteslice(0, position).to_s
41
+ end
42
+ end
43
+
44
+ # A function received an unsupported value type.
45
+ class InvalidTypeError < RuntimeError; end
46
+ # A value is invalid for the requested operation.
47
+ class InvalidValueError < RuntimeError; end
48
+ # A function received an incorrect number of arguments.
49
+ class InvalidArityError < RuntimeError; end
50
+ # A function name is not registered.
51
+ class UnknownFunctionError < RuntimeError; end
52
+ end
53
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JMESPath
4
+ # @api private
5
+ class Evaluator
6
+ private
7
+
8
+ def list(node, data)
9
+ return nil if data.nil?
10
+
11
+ node.children.map { |child| evaluate(child, data) }
12
+ end
13
+
14
+ def mapping(node, data)
15
+ return nil if data.nil?
16
+
17
+ node.children.to_h { |pair| [pair.value.to_s, evaluate(pair.children.fetch(0), data)] }
18
+ end
19
+ end
20
+ end