jamespath 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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
@@ -0,0 +1,4 @@
1
+ Gemfile.lock
2
+ .yardopts
3
+ .yardoc
4
+ doc
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
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
@@ -0,0 +1,6 @@
1
+ task :default => :test
2
+
3
+ desc 'Run all tests'
4
+ task :test do
5
+ sh "ruby #{FileList['test/**_test.rb'].join(' ')}"
6
+ end
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
@@ -0,0 +1,3 @@
1
+ module Jamespath
2
+ VERSION = '0.5.0'
3
+ end
@@ -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