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 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