hash-query 1.0.1
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.
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +29 -0
- data/Rakefile +11 -0
- data/hash_query.gemspec +17 -0
- data/lib/hash_query.rb +132 -0
- data/lib/hash_query/version.rb +3 -0
- data/test/helper.rb +4 -0
- data/test/test_hash_query.rb +126 -0
- metadata +58 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Mike Evans
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Hquery
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'hquery'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install hquery
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/hash_query.gemspec
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/hash_query/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ['Mike Evans']
|
6
|
+
gem.email = ['mike@urlgonomics.com']
|
7
|
+
gem.description = %q{Provides a css-like selector system for querying values out of deeply nested hashes}
|
8
|
+
gem.summary = %q{See above}
|
9
|
+
gem.homepage = 'https://github.com/mje113/hash-query'
|
10
|
+
|
11
|
+
gem.files = `git ls-files`.split($\)
|
12
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
13
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
14
|
+
gem.name = 'hash-query'
|
15
|
+
gem.require_paths = ['lib']
|
16
|
+
gem.version = HashQuery::VERSION
|
17
|
+
end
|
data/lib/hash_query.rb
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
require 'hash_query/version'
|
2
|
+
require 'pry'
|
3
|
+
|
4
|
+
module HashQuery
|
5
|
+
|
6
|
+
module Extension
|
7
|
+
|
8
|
+
# Public: Scans the hash data strucure looking for keys that match the
|
9
|
+
# psuedo css selector.
|
10
|
+
#
|
11
|
+
# selectors - String of pseudo css selectors to navigate hash structure
|
12
|
+
#
|
13
|
+
# Examples
|
14
|
+
#
|
15
|
+
# entity = {
|
16
|
+
# :a => 1,
|
17
|
+
# :b => {
|
18
|
+
# :c => {
|
19
|
+
# :d => {
|
20
|
+
# :e => 2,
|
21
|
+
# :f => 3
|
22
|
+
# }
|
23
|
+
# },
|
24
|
+
# :cc => [
|
25
|
+
# 3,
|
26
|
+
# {
|
27
|
+
# :g => 6,
|
28
|
+
# :h => 7,
|
29
|
+
# :i => [ 8, 9, 10, 11 ]
|
30
|
+
# },
|
31
|
+
# 4,
|
32
|
+
# 5
|
33
|
+
# ]
|
34
|
+
# }
|
35
|
+
# }.to_entity
|
36
|
+
# entity.query('a') # => 1
|
37
|
+
# entity.query('a b c d e') # => 2
|
38
|
+
# entity.query('c d e') # => 2
|
39
|
+
# entity.query('a>string')
|
40
|
+
# entity.query('a:string')
|
41
|
+
#
|
42
|
+
# Returns a value or collection of values that are found.
|
43
|
+
def query(selectors)
|
44
|
+
@_query ||= HashQuery::Query.new(self)
|
45
|
+
@_query.query(selectors)
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
50
|
+
class Query
|
51
|
+
|
52
|
+
def initialize(hash)
|
53
|
+
@hash = hash
|
54
|
+
end
|
55
|
+
|
56
|
+
def query(selectors)
|
57
|
+
selectors = Selector.parse(selectors)
|
58
|
+
found = descend(selectors, @hash, [])
|
59
|
+
|
60
|
+
if found.empty?
|
61
|
+
nil
|
62
|
+
elsif found.size == 1
|
63
|
+
found.first
|
64
|
+
else
|
65
|
+
found
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def hash_y?(node)
|
70
|
+
node.class.ancestors.include?(Hash)
|
71
|
+
end
|
72
|
+
|
73
|
+
def array_y?(node)
|
74
|
+
node.class.ancestors.include?(Array)
|
75
|
+
end
|
76
|
+
|
77
|
+
def descend(selectors, node, found)
|
78
|
+
return if node.nil? || (!hash_y?(node) && !array_y?(node))
|
79
|
+
descend_array(selectors, node, found) if array_y?(node)
|
80
|
+
descend_hash(selectors, node, found) if hash_y?(node)
|
81
|
+
return found
|
82
|
+
end
|
83
|
+
|
84
|
+
def descend_array(selectors, node, found)
|
85
|
+
node.each do |a|
|
86
|
+
descend(selectors, a, found)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def descend_hash(selectors, node, found)
|
91
|
+
selectors = selectors.dup
|
92
|
+
|
93
|
+
if selectors.first.match?(node)
|
94
|
+
selector = selectors.shift
|
95
|
+
if selectors.size == 0
|
96
|
+
found << selector.match!(node)
|
97
|
+
else
|
98
|
+
descend(selectors, selector.match!(node), found)
|
99
|
+
end
|
100
|
+
else
|
101
|
+
node.select { |k, node| hash_y?(node) || array_y?(node) }.each do |k, node|
|
102
|
+
descend(selectors, node, found)
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class Selector
|
109
|
+
|
110
|
+
def self.parse(selectors)
|
111
|
+
selectors.split(/ /).map { |matcher| new(matcher) }
|
112
|
+
end
|
113
|
+
|
114
|
+
def initialize(matcher)
|
115
|
+
@key, @type = matcher.split(':')
|
116
|
+
@type = Object.module_eval("::#{@type.capitalize}", __FILE__, __LINE__) if @type
|
117
|
+
end
|
118
|
+
|
119
|
+
def match?(node)
|
120
|
+
node.has_key?(@key) && (!@type || node[@key].is_a?(@type))
|
121
|
+
end
|
122
|
+
|
123
|
+
def match!(node)
|
124
|
+
node[@key]
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
end
|
129
|
+
|
130
|
+
class Hash
|
131
|
+
include HashQuery::Extension
|
132
|
+
end
|
data/test/helper.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
class TestHquery < MiniTest::Unit::TestCase
|
4
|
+
|
5
|
+
def setup
|
6
|
+
@movie = {
|
7
|
+
'title' => 'Goodfellas',
|
8
|
+
'rating' => 'R',
|
9
|
+
'encodings' => ['a', 'b', 'c'],
|
10
|
+
'media' => [
|
11
|
+
{
|
12
|
+
'type' => 'hd',
|
13
|
+
'encoding' => 'h264'
|
14
|
+
},
|
15
|
+
{
|
16
|
+
'type' => 'sd',
|
17
|
+
'encoding' => 'mpeg2'
|
18
|
+
}
|
19
|
+
],
|
20
|
+
'actors' => [
|
21
|
+
{
|
22
|
+
'name' => 'Robert De Niro',
|
23
|
+
'role' => 'James Conway',
|
24
|
+
'awards' => {
|
25
|
+
'academy' => 2,
|
26
|
+
'emmies' => 5
|
27
|
+
}
|
28
|
+
},
|
29
|
+
{
|
30
|
+
'name' => 'Ray Liotta',
|
31
|
+
'role' => 'Henry Hill'
|
32
|
+
}
|
33
|
+
],
|
34
|
+
'director' => {
|
35
|
+
'name' => 'Martin Scorsese',
|
36
|
+
'award_count' => 23
|
37
|
+
}
|
38
|
+
}
|
39
|
+
|
40
|
+
@hquery = {
|
41
|
+
'a' => 1,
|
42
|
+
'b' => {
|
43
|
+
'c' => {
|
44
|
+
'd' => {
|
45
|
+
'e' => 2,
|
46
|
+
'f' => 3
|
47
|
+
}
|
48
|
+
},
|
49
|
+
'cc' => [
|
50
|
+
3,
|
51
|
+
{
|
52
|
+
'g' => 6,
|
53
|
+
'h' => 7,
|
54
|
+
'i' => [ 8, 9, 10, 11 ]
|
55
|
+
},
|
56
|
+
4,
|
57
|
+
5
|
58
|
+
]
|
59
|
+
}
|
60
|
+
}
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_included_in_hash
|
64
|
+
h = {}
|
65
|
+
assert h.respond_to?(:query)
|
66
|
+
assert h.respond_to?(:query)
|
67
|
+
end
|
68
|
+
|
69
|
+
def test_finds_nothing
|
70
|
+
assert_nil @hquery.query('z')
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_finding_deep_array_value
|
74
|
+
assert_equal [8,9,10,11], @hquery.query('i')
|
75
|
+
end
|
76
|
+
|
77
|
+
def test_can_be_querried_shallow
|
78
|
+
assert_equal 'Goodfellas', @movie.query('title')
|
79
|
+
assert_equal 'R', @movie.query('rating')
|
80
|
+
end
|
81
|
+
|
82
|
+
def test_can_be_querried_one_level_deep
|
83
|
+
assert_equal 'Martin Scorsese', @movie.query('director name')
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_can_be_querried_shallow_for_a_deep_value
|
87
|
+
assert_equal 23, @movie.query('award_count')
|
88
|
+
end
|
89
|
+
|
90
|
+
def test_can_be_querried_two_levels_deep
|
91
|
+
assert_equal 2, @movie.query('actors awards academy')
|
92
|
+
assert_equal 5, @movie.query('actors awards emmies')
|
93
|
+
end
|
94
|
+
|
95
|
+
def test_can_be_querried_to_find_an_array
|
96
|
+
assert_equal ['a', 'b', 'c'], @movie.query('encodings')
|
97
|
+
end
|
98
|
+
|
99
|
+
def test_can_be_querried_for_multiple_values
|
100
|
+
assert_equal ['Robert De Niro', 'Ray Liotta'], @movie.query('actors name')
|
101
|
+
end
|
102
|
+
|
103
|
+
def test_can_query_for_type
|
104
|
+
assert_equal 'Goodfellas', @movie.query('title:string')
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_can_deep_query_for_type
|
108
|
+
assert_equal 2, @movie.query('academy:fixnum')
|
109
|
+
assert_equal 2, @movie.query('actors awards academy:fixnum')
|
110
|
+
end
|
111
|
+
|
112
|
+
def test_will_return_nil_if_type_is_not_matched
|
113
|
+
assert_equal nil, @movie.query('title:fixnum')
|
114
|
+
end
|
115
|
+
|
116
|
+
def test_can_query_for_complex_type
|
117
|
+
assert_equal ['a', 'b', 'c'], @movie.query('encodings:array')
|
118
|
+
end
|
119
|
+
|
120
|
+
def test_raises_exception_if_type_not_found
|
121
|
+
assert_raises NameError do
|
122
|
+
@movie.query('title:bogus')
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
end
|
metadata
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hash-query
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 1.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Mike Evans
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-05-30 00:00:00.000000000 Z
|
13
|
+
dependencies: []
|
14
|
+
description: Provides a css-like selector system for querying values out of deeply
|
15
|
+
nested hashes
|
16
|
+
email:
|
17
|
+
- mike@urlgonomics.com
|
18
|
+
executables: []
|
19
|
+
extensions: []
|
20
|
+
extra_rdoc_files: []
|
21
|
+
files:
|
22
|
+
- .gitignore
|
23
|
+
- Gemfile
|
24
|
+
- LICENSE
|
25
|
+
- README.md
|
26
|
+
- Rakefile
|
27
|
+
- hash_query.gemspec
|
28
|
+
- lib/hash_query.rb
|
29
|
+
- lib/hash_query/version.rb
|
30
|
+
- test/helper.rb
|
31
|
+
- test/test_hash_query.rb
|
32
|
+
homepage: https://github.com/mje113/hash-query
|
33
|
+
licenses: []
|
34
|
+
post_install_message:
|
35
|
+
rdoc_options: []
|
36
|
+
require_paths:
|
37
|
+
- lib
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
39
|
+
none: false
|
40
|
+
requirements:
|
41
|
+
- - ! '>='
|
42
|
+
- !ruby/object:Gem::Version
|
43
|
+
version: '0'
|
44
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
45
|
+
none: false
|
46
|
+
requirements:
|
47
|
+
- - ! '>='
|
48
|
+
- !ruby/object:Gem::Version
|
49
|
+
version: '0'
|
50
|
+
requirements: []
|
51
|
+
rubyforge_project:
|
52
|
+
rubygems_version: 1.8.17
|
53
|
+
signing_key:
|
54
|
+
specification_version: 3
|
55
|
+
summary: See above
|
56
|
+
test_files:
|
57
|
+
- test/helper.rb
|
58
|
+
- test/test_hash_query.rb
|