jamespath 0.5.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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/Gemfile +3 -0
- data/LICENSE +21 -0
- data/README.md +53 -0
- data/Rakefile +6 -0
- data/jamespath.gemspec +15 -0
- data/lib/jamespath.rb +34 -0
- data/lib/jamespath/parser.rb +120 -0
- data/lib/jamespath/tokenizer.rb +57 -0
- data/lib/jamespath/version.rb +3 -0
- data/lib/jamespath/vm.rb +158 -0
- data/test/compliance/basic.json +92 -0
- data/test/compliance/escape.json +46 -0
- data/test/compliance/indices.json +424 -0
- data/test/compliance/multiselect.json +214 -0
- data/test/compliance/ormatch.json +46 -0
- data/test/compliance/wildcard.json +100 -0
- data/test/compliance_test.rb +29 -0
- data/test/struct_test.rb +23 -0
- data/test/tokenizer_test.rb +15 -0
- metadata +108 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 71c755c55c46dffb46d63967c271a3d3f0277bc9
|
4
|
+
data.tar.gz: bb5008e347cfd92fda04ef9aea28a21f3da6361b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9a467a6dcbf41838c90dfc2dbabf598525c1c6325646797215c0a0d2b5b0234559be7f2bc48b6f4f4173c4ebd6b86aba8e1e67e2fa18f9832e3edbf229baad38
|
7
|
+
data.tar.gz: 72f1457d53b8bdcf42193a1044ba0d34ac258baca583f3796b8192f4e41d8039cc118db8aaa963d3df9dd4c50a7eba3143bf51d1c37150d719789a9a31edd369
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2013 Loren Segal and Trevor Rowe
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# Jamespath
|
2
|
+
|
3
|
+
Jamespath is a library that lets you select objects from deeply nested
|
4
|
+
structures, arrays, hashes, or JSON objects using a simple expression
|
5
|
+
language.
|
6
|
+
|
7
|
+
Think XPath, but for objects.
|
8
|
+
|
9
|
+
## Installing
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
$ gem install jamespath
|
13
|
+
```
|
14
|
+
|
15
|
+
Or with Bundler:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
gem 'jamespath', '~> 1.0'
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
To use Jamespath, call the {Jamespath.search} method with an expression
|
24
|
+
and an object ot search:
|
25
|
+
|
26
|
+
```ruby
|
27
|
+
object = { foo: { bar: ['value1', 'value2', 'value3'] } }
|
28
|
+
Jamespath.search('foo.bar[0]', object) #=> 'value1'
|
29
|
+
```
|
30
|
+
|
31
|
+
You can also {Jamespath.compile} an expression if you are performing the same
|
32
|
+
search operation against multiple objects:
|
33
|
+
|
34
|
+
```ruby
|
35
|
+
object1 = { foo: { bar: ['value1', 'value2', 'value3'] } }
|
36
|
+
object2 = { foo: { bar: ['value4', 'value5', 'value6'] } }
|
37
|
+
|
38
|
+
expr = Jamespath.compile('foo.bar[0]')
|
39
|
+
expr.search(object1) #=> 'value1'
|
40
|
+
expr.search(object2) #=> 'value4'
|
41
|
+
```
|
42
|
+
|
43
|
+
## Expression Syntax
|
44
|
+
|
45
|
+
See the [JMESpath][1] project for more information on the expression syntax.
|
46
|
+
|
47
|
+
## License & Acknowledgements
|
48
|
+
|
49
|
+
This library was written by Loren Segal and Trevor Rowe and is licensed under
|
50
|
+
the MIT license. The implementation is based on the [JMESpath][1] library
|
51
|
+
written by James Sayerwinnie for the Python programming language.
|
52
|
+
|
53
|
+
[1]: http://github.com/boto/jmespath
|
data/Rakefile
ADDED
data/jamespath.gemspec
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 'lib', 'jamespath', 'version')
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = 'jamespath'
|
5
|
+
spec.version = Jamespath::VERSION
|
6
|
+
spec.summary = 'Implements JMESpath declarative object searching.'
|
7
|
+
spec.description = 'Like XPath, but for JSON and other structured objects.'
|
8
|
+
spec.authors = ['Loren Segal', 'Trevor Rowe']
|
9
|
+
spec.homepage = 'http://github.com/lsegal/jamespath'
|
10
|
+
spec.license = 'MIT'
|
11
|
+
spec.files = `git ls-files`.split($/)
|
12
|
+
spec.test_files = spec.files.grep(%r{^test/})
|
13
|
+
spec.add_development_dependency('yard', '~> 0.0')
|
14
|
+
spec.add_development_dependency('rdiscount', '>= 2.1.7', '< 3.0')
|
15
|
+
end
|
data/lib/jamespath.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
require_relative './jamespath/tokenizer'
|
2
|
+
require_relative './jamespath/parser'
|
3
|
+
require_relative './jamespath/vm'
|
4
|
+
require_relative './jamespath/version'
|
5
|
+
|
6
|
+
# {include:file:README.md}
|
7
|
+
module Jamespath
|
8
|
+
module_function
|
9
|
+
|
10
|
+
# Searches an object with a given JMESpath expression.
|
11
|
+
#
|
12
|
+
# @param query [String] the expression to search for.
|
13
|
+
# @param object [Object] an object to search for the expression in.
|
14
|
+
# @return [Object] the object, or list of objects, that match the expression.
|
15
|
+
# @return [nil] if no objects matched the expression
|
16
|
+
# @example Searching an object
|
17
|
+
# Jamespath.search('foo.bar', foo: {bar: 'result'}) #=> 'result'
|
18
|
+
def search(query, object)
|
19
|
+
compile(query).search(object)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Compiles an expression that can be {VM#search searched}.
|
23
|
+
#
|
24
|
+
# @param query [String] the expression to search for.
|
25
|
+
# @return [VM] a virtual machine object that can interpret the expression.
|
26
|
+
# @see VM#search
|
27
|
+
# @example Compiling an expression
|
28
|
+
# expr = Jamespath.compile('foo.bar')
|
29
|
+
# expr.search(foo: {bar: 'result1'}) #=> 'result1'
|
30
|
+
# expr.search(foo: {bar: 'result2'}) #=> 'result2'
|
31
|
+
def compile(query)
|
32
|
+
VM.new(Parser.new.parse(query))
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,120 @@
|
|
1
|
+
require_relative 'tokenizer'
|
2
|
+
|
3
|
+
module Jamespath
|
4
|
+
# # Grammar
|
5
|
+
#
|
6
|
+
# ```abnf
|
7
|
+
# expression : sub_expression | index_expression
|
8
|
+
# | or_expression | identifier | '*'
|
9
|
+
# | multi_select_list | multi_select_hash;
|
10
|
+
# sub_expression : expression '.' expression;
|
11
|
+
# or_expression : expression '||' expression;
|
12
|
+
# index_expression : expression bracket_specifier | bracket_specifier;
|
13
|
+
# multi_select_list : '[' non_branched_expr ']';
|
14
|
+
# multi_select_hash : '{' keyval_expr '}';
|
15
|
+
# keyval_expr : identifier ':' non_branched_expr;
|
16
|
+
# non_branched_expr : identifier
|
17
|
+
# | non_branched_expr '.' identifier
|
18
|
+
# | non_branched_expr '[' number ']';
|
19
|
+
# bracket_specifier : '[' number ']' | '[' '*' ']';
|
20
|
+
# ```
|
21
|
+
class Parser
|
22
|
+
# Parses an expression into a set of instructions to be executed by the
|
23
|
+
# {VM}.
|
24
|
+
#
|
25
|
+
# @param source [String] the expression to parse
|
26
|
+
# @return [Array(Symbol, Object)] a set of instructions
|
27
|
+
# @see VM
|
28
|
+
def parse(source)
|
29
|
+
@tokens = Tokenizer.new.tokenize(source)
|
30
|
+
@idx = 0
|
31
|
+
@instructions = []
|
32
|
+
parse_expression
|
33
|
+
@instructions
|
34
|
+
end
|
35
|
+
|
36
|
+
protected
|
37
|
+
|
38
|
+
def parse_expression
|
39
|
+
next_token! do |token|
|
40
|
+
case token.type
|
41
|
+
when :asterisk
|
42
|
+
@instructions << [:get_key_all, nil]
|
43
|
+
when :identifier, :number
|
44
|
+
@instructions << [:get_key, token.value]
|
45
|
+
when :lbrace # multi_select_hash
|
46
|
+
parse_multi_select_hash
|
47
|
+
when :lbracket # list
|
48
|
+
parse_index_expression
|
49
|
+
else
|
50
|
+
unexpected
|
51
|
+
end
|
52
|
+
|
53
|
+
parse_sub_expression
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def parse_sub_expression
|
58
|
+
next_token do |token|
|
59
|
+
case token.type
|
60
|
+
when :dot
|
61
|
+
parse_expression
|
62
|
+
when :double_pipe
|
63
|
+
@instructions << [:ret_if_match, nil]
|
64
|
+
parse_expression
|
65
|
+
when :lbracket
|
66
|
+
parse_index_expression
|
67
|
+
else
|
68
|
+
unexpected
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def parse_index_expression
|
74
|
+
next_token! do |token|
|
75
|
+
case token.type
|
76
|
+
when :number
|
77
|
+
assert_next_type :rbracket
|
78
|
+
@instructions << [:get_idx, token.value.to_i]
|
79
|
+
parse_sub_expression
|
80
|
+
when :asterisk
|
81
|
+
assert_next_type :rbracket
|
82
|
+
@instructions << [:get_idx_all, nil]
|
83
|
+
parse_sub_expression
|
84
|
+
when :rbracket
|
85
|
+
@instructions << [:flatten_list, nil]
|
86
|
+
parse_sub_expression
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
private
|
92
|
+
|
93
|
+
def assert_next_type(type)
|
94
|
+
next_token! do |token|
|
95
|
+
unexpected(token) unless token.type == type
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def next_token!(peek = false, &block)
|
100
|
+
yielded = false
|
101
|
+
next_token(peek) {|token| yield(token); yielded = true }
|
102
|
+
unexpected(nil) unless yielded
|
103
|
+
end
|
104
|
+
|
105
|
+
def next_token(peek = false, &block)
|
106
|
+
if token = @tokens[@idx]
|
107
|
+
@idx += 1 unless peek
|
108
|
+
yield(token)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def unexpected(token = @tokens[@idx-1])
|
113
|
+
if token
|
114
|
+
raise SyntaxError, "unexpected token #{token}"
|
115
|
+
else
|
116
|
+
raise SyntaxError, "unexpected end-of-input at #{@tokens.last}"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'strscan'
|
2
|
+
|
3
|
+
module Jamespath
|
4
|
+
class Token < Struct.new(:type, :value, :pos)
|
5
|
+
def inspect; "#{type}(#{value.inspect}, pos=#{pos})" end
|
6
|
+
alias to_s inspect
|
7
|
+
end
|
8
|
+
|
9
|
+
class Tokenizer
|
10
|
+
attr_reader :tokens
|
11
|
+
|
12
|
+
TOKENS = {
|
13
|
+
lbracket: /\[/,
|
14
|
+
rbracket: /\]/,
|
15
|
+
lbrace: /\{/,
|
16
|
+
rbrace: /\}/,
|
17
|
+
comma: /,/,
|
18
|
+
dot: /\./,
|
19
|
+
colon: /:/,
|
20
|
+
double_pipe: /\|\|/,
|
21
|
+
asterisk: /\*/,
|
22
|
+
number: /-?[0-9]+/,
|
23
|
+
quoted_identifier: /"([^"\\]|\\"|\\\\|\\[^"])*"/,
|
24
|
+
identifier: /[a-zA-Z0-9_\u007E-\uFFFF]+/
|
25
|
+
}
|
26
|
+
|
27
|
+
def tokenize(source)
|
28
|
+
@pos = 0
|
29
|
+
@source = source
|
30
|
+
@scanner = StringScanner.new(source)
|
31
|
+
@tokens = []
|
32
|
+
until @scanner.eos?
|
33
|
+
@tokens << next_token
|
34
|
+
end
|
35
|
+
@tokens
|
36
|
+
end
|
37
|
+
|
38
|
+
protected
|
39
|
+
|
40
|
+
def next_token
|
41
|
+
@pos += @scanner.skip(/\s+/) || 0
|
42
|
+
TOKENS.each do |type, re|
|
43
|
+
if token = @scanner.scan(re) and token.length > 0
|
44
|
+
pos, @pos = @pos, @pos + token.length
|
45
|
+
if type == :quoted_identifier
|
46
|
+
type = :identifier
|
47
|
+
token = token[1...-1].gsub(/\\"/, '"')
|
48
|
+
end
|
49
|
+
|
50
|
+
return Token.new(type, token, pos)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
raise SyntaxError, "unexpected token at pos=#{@pos}: #{@source[@pos]}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/jamespath/vm.rb
ADDED
@@ -0,0 +1,158 @@
|
|
1
|
+
module Jamespath
|
2
|
+
# The virtual machine that interprets compiled expressions and searches for
|
3
|
+
# objects. The VM implements a handful of instructions that can be used to
|
4
|
+
# navigate through an object structure.
|
5
|
+
#
|
6
|
+
# # VM Overview
|
7
|
+
#
|
8
|
+
# The VM iterates over the instructions attempting to navigate through
|
9
|
+
# the given object. As instructions are evaluated, the "object" is tracked
|
10
|
+
# and replaced as each selection is made. The result of a search is the
|
11
|
+
# object value after evaluating all instructions.
|
12
|
+
#
|
13
|
+
# The VM understands "hash-like" and "array-like" objects. "Array-like"
|
14
|
+
# objects are defined as any object that subclasses Array. "Hash-like"
|
15
|
+
# objects are defined as either Hash or Struct objects.
|
16
|
+
#
|
17
|
+
# ## Instruction list
|
18
|
+
#
|
19
|
+
# ### `:get_key <key>`
|
20
|
+
#
|
21
|
+
# Gets a "key" from the hash-like object on the stack. If the object is
|
22
|
+
# not hash-like, this instruction sets the object value to nil.
|
23
|
+
#
|
24
|
+
# ### `:get_idx <idx>`
|
25
|
+
#
|
26
|
+
# Gets an object at index "idx" from the array-like object on the stack.
|
27
|
+
# If the object is not array-like, this instruction sets the object value
|
28
|
+
# to nil. "idx" must be a number, but can be negative. Negative values
|
29
|
+
# index from the end of the array, where -1 is the last value.
|
30
|
+
#
|
31
|
+
# ### `:get_key_all`
|
32
|
+
#
|
33
|
+
# Gets all values from the hash-like object value. If the object is not
|
34
|
+
# hash-like, this instruction sets the object value to nil.
|
35
|
+
#
|
36
|
+
# ### `:get_idx_all`
|
37
|
+
#
|
38
|
+
# Gets all items from an array-like object. If the object is hash-like,
|
39
|
+
# the object is set to the keys of the hash-like structure. If the object
|
40
|
+
# is not array-like or hash-like, this instruction sets the object value
|
41
|
+
# to nil.
|
42
|
+
#
|
43
|
+
# ### `:flatten_list`
|
44
|
+
#
|
45
|
+
# Flattens a list of subarrays into a single array. If the object is not
|
46
|
+
# array-like, this instruction sets the object value to an empty array.
|
47
|
+
#
|
48
|
+
# ### `:ret_if_match`
|
49
|
+
#
|
50
|
+
# Breaks from parsing instructions if the object value is non-nil. If the
|
51
|
+
# object is nil, this instruction should reset the object value to the
|
52
|
+
# original object that was being searched.
|
53
|
+
#
|
54
|
+
class VM
|
55
|
+
# @api private
|
56
|
+
class ArrayGroup < Array
|
57
|
+
def initialize(arr) replace(arr) end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Array(Symbol, Object)] the instructions the VM executes.
|
61
|
+
attr_reader :instructions
|
62
|
+
|
63
|
+
# Creates a virtual machine that can evaluate a set of instructions.
|
64
|
+
# Use the {Parser} to turn an expression into a set of instructions.
|
65
|
+
#
|
66
|
+
# @param instructions [Array(Symbol, Object)] a list of instructions to
|
67
|
+
# execute.
|
68
|
+
# @see Parser#parse
|
69
|
+
# @example VM for expression "foo.bar[-1]"
|
70
|
+
# vm = VM.new [
|
71
|
+
# [:get_key, 'foo'],
|
72
|
+
# [:get_key, 'bar'],
|
73
|
+
# [:get_idx, -1]
|
74
|
+
# ]
|
75
|
+
# vm.search(foo: {bar: [1, 2, 3]}) #=> 3
|
76
|
+
def initialize(instructions)
|
77
|
+
@instructions = instructions
|
78
|
+
end
|
79
|
+
|
80
|
+
# Searches for the compile expression against the object passed in.
|
81
|
+
#
|
82
|
+
# @param object_to_search [Object] the object to search for results.
|
83
|
+
# @return (see Jamespath.search)
|
84
|
+
def search(object_to_search)
|
85
|
+
object = object_to_search
|
86
|
+
@instructions.each do |instruction|
|
87
|
+
if instruction.first == :ret_if_match
|
88
|
+
if object
|
89
|
+
break # short-circuit or expression
|
90
|
+
else
|
91
|
+
object = object_to_search # reset search
|
92
|
+
end
|
93
|
+
else
|
94
|
+
object = send(instruction[0], object, instruction[1])
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
object
|
99
|
+
end
|
100
|
+
|
101
|
+
protected
|
102
|
+
|
103
|
+
def get_key(object, key)
|
104
|
+
if struct?(object)
|
105
|
+
object[key]
|
106
|
+
elsif ArrayGroup === object
|
107
|
+
object = object.map {|o| get_key(o, key) }.compact
|
108
|
+
object.length > 0 ? ArrayGroup.new(object) : []
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def get_idx(object, idx)
|
113
|
+
if ArrayGroup === object
|
114
|
+
object = object.map {|o| get_idx(o, idx) }.compact
|
115
|
+
object.length > 0 ? ArrayGroup.new(object) : nil
|
116
|
+
elsif array?(object)
|
117
|
+
object[idx]
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
def get_key_all(object, *)
|
122
|
+
object.respond_to?(:values) ? ArrayGroup.new(object.values) : nil
|
123
|
+
end
|
124
|
+
|
125
|
+
def get_idx_all(object, *)
|
126
|
+
if array?(object)
|
127
|
+
new_object = object.map do |o|
|
128
|
+
Array === o ? ArrayGroup.new(o) : o
|
129
|
+
end
|
130
|
+
ArrayGroup.new(new_object)
|
131
|
+
elsif object.respond_to?(:keys)
|
132
|
+
ArrayGroup.new(object.keys)
|
133
|
+
elsif object.respond_to?(:members)
|
134
|
+
ArrayGroup.new(object.members.map(&:to_s))
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def flatten_list(object, *)
|
139
|
+
if array?(object)
|
140
|
+
new_object = []
|
141
|
+
object.each {|o| array?(o) ? (new_object += o) : new_object << o }
|
142
|
+
ArrayGroup.new(new_object)
|
143
|
+
else
|
144
|
+
[]
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
|
150
|
+
def struct?(object)
|
151
|
+
Hash === object || Struct === object
|
152
|
+
end
|
153
|
+
|
154
|
+
def array?(object)
|
155
|
+
Array === object
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|