jamespath 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|