queryable_array 0.0.0 → 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README.rdoc +37 -3
- data/Rakefile +5 -1
- data/lib/queryable_array.rb +1 -0
- data/lib/queryable_array/default_finder.rb +25 -9
- data/lib/queryable_array/dot_notation.rb +18 -7
- data/lib/queryable_array/dynamic_finder.rb +4 -1
- data/lib/queryable_array/queryable.rb +20 -7
- data/lib/queryable_array/shorthand.rb +7 -3
- data/lib/queryable_array/version.rb +1 -1
- data/test/queryable_array_test.rb +10 -0
- metadata +2 -2
data/README.rdoc
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
{<img src="https://secure.travis-ci.org/shuber/queryable_array.png"/>}[http://travis-ci.org/shuber/queryable_array]
|
4
4
|
{<img src="https://gemnasium.com/shuber/queryable_array.png"/>}[https://gemnasium.com/shuber/queryable_array]
|
5
|
-
{<img src="https://d25lcipzij17d.cloudfront.net/badge.png?v=0.0.
|
5
|
+
{<img src="https://d25lcipzij17d.cloudfront.net/badge.png?v=0.0.1"/>}[http://rubygems.org/gems/queryable_array]
|
6
6
|
{<img src="https://codeclimate.com/badge.png" />}[https://codeclimate.com/github/shuber/queryable_array]
|
7
7
|
|
8
8
|
A +QueryableArray+ inherits from +Array+ and is intended to store a group of
|
@@ -12,6 +12,7 @@ for looking up objects by querying their attributes.
|
|
12
12
|
|
13
13
|
View the full documentation over at rubydoc.info[http://rubydoc.info/github/shuber/queryable_array/frames].
|
14
14
|
|
15
|
+
|
15
16
|
== Installation
|
16
17
|
|
17
18
|
gem install queryable_array
|
@@ -25,7 +26,7 @@ Initialize the +QueryableArray+ with a collection of objects e.g. +Page+ objects
|
|
25
26
|
|
26
27
|
pages = QueryableArray.new Page.all
|
27
28
|
|
28
|
-
The +pages+ object can then be queried by passing a search hash to the
|
29
|
+
The +pages+ object can then be queried by passing a search hash to the <tt>[]</tt> method
|
29
30
|
|
30
31
|
pages[uri: '/'] # => #<Page @uri='/' @name='Home'>
|
31
32
|
pages[name: 'About'] # => #<Page @uri='/about' @name='About'>
|
@@ -121,4 +122,37 @@ e.g. <tt>pages.sitemap!</tt> which calls <tt>pages['sitemap']</tt>
|
|
121
122
|
You may also query to see if a match exists by appending a <tt>?</tt> to your search
|
122
123
|
|
123
124
|
pages.sitemap? # => true
|
124
|
-
pages.missing? # => false
|
125
|
+
pages.missing? # => false
|
126
|
+
|
127
|
+
|
128
|
+
=== Composable
|
129
|
+
|
130
|
+
Functionality for +QueryableArray+ has been separated out into individual modules
|
131
|
+
containing their own features which allows you to create your own objects and only
|
132
|
+
include the features you care about
|
133
|
+
|
134
|
+
* <tt>QueryableArray::DefaultFinder</tt> - Allows objects to be searched by +default_finders+ thru <tt>[]</tt>
|
135
|
+
* <tt>QueryableArray::DotNotation</tt> - Allows objects to be searched using dot notation thru +method_missing+ which behaves like an alias to <tt>QueryableArray::DefaultFinder#[]</tt>
|
136
|
+
* <tt>QueryableArray::DynamicFinder</tt> - Allows objects to be searched by dynamic finders thru +method_missing+ similar to the ActiveRecord dynamic attribute-based finders e.g. +find_by_email+ or +find_all_by_last_name+
|
137
|
+
* <tt>QueryableArray::Queryable</tt> - Allows +find_by+ and +find_all+ to accept search hashes which are converted into +Proc+ searches and passed as the block arguments for +find+ and +find_all+ respectively
|
138
|
+
* <tt>QueryableArray::Shorthand</tt> - Makes <tt>[search_hash]</tt> and <tt>[[search_hash]]</tt> behave as an alias for +find_by+ and +find_all+ respectively
|
139
|
+
|
140
|
+
|
141
|
+
=== Real world example
|
142
|
+
|
143
|
+
Try using it inside of your templates:
|
144
|
+
|
145
|
+
<div class="posts">
|
146
|
+
<%- posts[[published: true]].each do |post| -%>
|
147
|
+
<div class="post">
|
148
|
+
<h2><a href="<%= post.url -%>"><%= post.title -%></a></h2>
|
149
|
+
<div class="excerpt"><%= post.excerpt -%></div>
|
150
|
+
<a href="<%= post.url -%>#comments"><%= post.comments[[approved: true]].size -%> comments</a>
|
151
|
+
</div>
|
152
|
+
<%- end -%>
|
153
|
+
</div>
|
154
|
+
|
155
|
+
|
156
|
+
== Testing
|
157
|
+
|
158
|
+
bundle exec rake
|
data/Rakefile
CHANGED
data/lib/queryable_array.rb
CHANGED
@@ -3,6 +3,7 @@ require 'queryable_array/dot_notation'
|
|
3
3
|
require 'queryable_array/dynamic_finder'
|
4
4
|
require 'queryable_array/queryable'
|
5
5
|
require 'queryable_array/shorthand'
|
6
|
+
require 'queryable_array/version'
|
6
7
|
|
7
8
|
# A +QueryableArray+ inherits from +Array+ and is intended to store a group of
|
8
9
|
# objects which share the same attributes allowing them to be searched. It
|
@@ -1,4 +1,9 @@
|
|
1
1
|
class QueryableArray < Array
|
2
|
+
# Allows objects to be searched by +default_finders+ thru <tt>[]</tt>. For example:
|
3
|
+
#
|
4
|
+
# users = QueryableArray.new(User.all, :email)
|
5
|
+
# users['test@example.com'] # => #<User @email='test@example.com'>
|
6
|
+
# users['missing@domain.com'] # => nil
|
2
7
|
module DefaultFinder
|
3
8
|
attr_accessor :default_finders
|
4
9
|
|
@@ -11,19 +16,26 @@ class QueryableArray < Array
|
|
11
16
|
self.default_finders = Array(default_finders)
|
12
17
|
end
|
13
18
|
|
14
|
-
# If +default_finders+ has been set and +key+ is not a +Fixnum+
|
15
|
-
#
|
16
|
-
#
|
17
|
-
#
|
18
|
-
# then it acts like an alias for +find_by+
|
19
|
-
#
|
19
|
+
# If +default_finders+ has been set and +key+ is not a +Fixnum+, +Range+,
|
20
|
+
# or anything else natively supported by +Array+ then it loops thru each
|
21
|
+
# +default_finders+ and returns the first matching result of +find_by(finder => key)+
|
22
|
+
# or +find_all(finder => key.first)+ if +key+ is an +Array+. If +key+ is already
|
23
|
+
# a +Hash+ or an +Array+ containing a +Hash+ then it acts like an alias for +find_by+
|
24
|
+
# or +find_all+ respectively. It also accepts a +Proc+ or any object that responds to
|
25
|
+
# +call+. It behaves exactly like its superclass +Array+ in all other cases.
|
20
26
|
#
|
21
27
|
# pages = QueryableArray.new(Page.all, [:uri, :name])
|
22
28
|
#
|
23
|
-
# pages['/'] # => #<Page @uri='/'>
|
24
|
-
# pages['Home'] # => #<Page @name='Home'>
|
25
|
-
# pages[/home/i] # => #<Page @name='Home'>
|
29
|
+
# pages['/'] # => #<Page @uri='/' @name='Home'>
|
30
|
+
# pages['Home'] # => #<Page @uri='/' @name='Home'>
|
31
|
+
# pages[/home/i] # => #<Page @uri='/' @name='Home'>
|
26
32
|
# pages['missing'] # => nil
|
33
|
+
#
|
34
|
+
# pages[[/users/i]] # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>]
|
35
|
+
# pages[[/missing/i]] # => []
|
36
|
+
#
|
37
|
+
# pages[proc { |page| page.uri == '/' }] # => #<Page @uri='/' @name='Home'>
|
38
|
+
# pages[[proc { |page| page.uri =~ /users/i }]] # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>]
|
27
39
|
def [](key)
|
28
40
|
super
|
29
41
|
rescue TypeError => error
|
@@ -35,6 +47,10 @@ class QueryableArray < Array
|
|
35
47
|
end
|
36
48
|
end
|
37
49
|
|
50
|
+
# Converts a search into a +Proc+ object that can be passed to +find_by+ or
|
51
|
+
# +find_all+. If +search+ is a +Proc+ or an object that responds to +call+
|
52
|
+
# then it is wrapped in a +Proc+ and returned. Otherwise the returned +Proc+
|
53
|
+
# loops thru each +default_finders+ looking for a value that matches +search+.
|
38
54
|
def query(search)
|
39
55
|
Proc.new do |object|
|
40
56
|
proc = search.respond_to?(:call) ? search : Proc.new do |object|
|
@@ -1,23 +1,32 @@
|
|
1
1
|
require 'queryable_array/default_finder'
|
2
2
|
|
3
3
|
class QueryableArray < Array
|
4
|
+
# Allows objects to be searched using dot notation thru +method_missing+
|
5
|
+
# which behaves like an alias to <tt>QueryableArray::DefaultFinder#[]</tt>
|
4
6
|
module DotNotation
|
5
7
|
def self.included(base)
|
6
8
|
base.send :include, DefaultFinder
|
7
9
|
end
|
8
10
|
|
9
|
-
# If +method_name+ does not have a <tt>!</tt> or <tt>?</tt> suffix then
|
11
|
+
# If +method_name+ does not have a <tt>!</tt> or <tt>?</tt> suffix then <tt>self[/#{method_name}/i]</tt>
|
10
12
|
# is returned. If it returns +nil+ or raises +TypeError+ (no +default_finders+) then +super+ is returned.
|
11
13
|
#
|
12
|
-
# If +method_name+ ends in a <tt>!</tt> then
|
14
|
+
# If +method_name+ ends in a <tt>!</tt> then <tt>self[method_name]</tt> (without the <tt>!</tt>) is returned. If +method_name+
|
13
15
|
# ends with a <tt>?</tt> then a boolean is returned determining whether or not a match was found.
|
14
16
|
#
|
15
17
|
# users = QueryableArray.new User.all, :username
|
16
18
|
#
|
17
|
-
# users.bob
|
18
|
-
# users.BOB
|
19
|
-
# users.missing
|
20
|
-
# QueryableArray.new.missing
|
19
|
+
# users.bob # => #<User @username='bob'>
|
20
|
+
# users.BOB # => #<User @username='bob'>
|
21
|
+
# users.missing # => NoMethodError
|
22
|
+
# QueryableArray.new.missing # => NoMethodError
|
23
|
+
#
|
24
|
+
# users.bob! # => #<User @username='bob'>
|
25
|
+
# users.BOB! # => NoMethodError
|
26
|
+
#
|
27
|
+
# users.bob? # => true
|
28
|
+
# users.BOB? # => true
|
29
|
+
# users.missing? # => false
|
21
30
|
def method_missing(method_name, *arguments)
|
22
31
|
if method_name.to_s =~ /^(.+?)([\!\?])?$/
|
23
32
|
search = $2 == '!' ? $1 : /#{$1}/i
|
@@ -30,8 +39,10 @@ class QueryableArray < Array
|
|
30
39
|
end
|
31
40
|
end
|
32
41
|
|
42
|
+
# Checks if +method_name+ can be handled by +method_missing+ and
|
43
|
+
# and delegates the call to +super+ otherwise.
|
33
44
|
def respond_to_missing?(method_name, include_super)
|
34
|
-
!!(method_name.to_s
|
45
|
+
!!(method_name.to_s =~ /\?$/ || super || send(method_name))
|
35
46
|
rescue NoMethodError
|
36
47
|
false
|
37
48
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
class QueryableArray < Array
|
2
2
|
module DynamicFinder
|
3
|
+
# Allows objects to be searched by dynamic finders thru +method_missing+ similar
|
4
|
+
# to the ActiveRecord dynamic attribute-based finders e.g. +find_by_email+ or
|
5
|
+
# +find_all_by_last_name+
|
3
6
|
def self.included(base)
|
4
7
|
base.send :alias_method, :find_all_by, :find_all
|
5
8
|
end
|
@@ -35,7 +38,7 @@ class QueryableArray < Array
|
|
35
38
|
def method_missing(method_name, *arguments)
|
36
39
|
if query = finder?(method_name)
|
37
40
|
search = Hash[query[:attributes].split('_and_').zip(arguments)]
|
38
|
-
send "find_#{query[:type]}", search
|
41
|
+
send "find_#{query[:type].downcase}", search
|
39
42
|
else
|
40
43
|
super
|
41
44
|
end
|
@@ -1,16 +1,19 @@
|
|
1
1
|
class QueryableArray < Array
|
2
|
+
# Allows +find_by+ and +find_all+ to accept search hashes which are
|
3
|
+
# converted into +Proc+ searches and passed as the block arguments for
|
4
|
+
# +find+ and +find_all+ respectively
|
2
5
|
module Queryable
|
3
|
-
# Returns a
|
4
|
-
# is specified, it behaves exactly like
|
5
|
-
# +search+ hash is converted into
|
6
|
-
# argument to +Enumerable#find_all+.
|
6
|
+
# Returns a dup'd +Queryable+ replaced with objects matching the +search+
|
7
|
+
# criteria. When a +block+ is specified, it behaves exactly like
|
8
|
+
# +Enumerable#find_all+. Otherwise the +search+ hash is converted into
|
9
|
+
# a +finder+ proc and passed as the block argument to +Enumerable#find_all+.
|
7
10
|
#
|
8
11
|
# users.find_all(age: 30) # => [#<User @age=30>, #<User @age=30>, ...]
|
9
12
|
# users.find_all(name: 'missing') # => []
|
10
13
|
# users.find_all { |user| user.age < 30 } # => [#<User @age=22>, #<User @age=26>, ...]
|
11
14
|
def find_all(search = {}, &block)
|
12
15
|
block = finder search unless block_given?
|
13
|
-
|
16
|
+
dup.replace super(&block)
|
14
17
|
end
|
15
18
|
|
16
19
|
# Behaves exactly like +find_all+ but only returns the first match. If no match
|
@@ -24,14 +27,24 @@ class QueryableArray < Array
|
|
24
27
|
end
|
25
28
|
|
26
29
|
# Accepts a +search+ hash and returns a +Proc+ which determines if all of an
|
27
|
-
# object's attributes match their expected
|
30
|
+
# object's searched attributes match their expected values. It can be used as
|
28
31
|
# the block arguments for +find+, +find_by+ and +find_all+.
|
29
32
|
#
|
30
|
-
#
|
33
|
+
# Values are compared with expected values using <tt>==</tt> or <tt>===</tt>.
|
34
|
+
# If the expected value is a +Proc+ or anything that responds to +call+ then
|
35
|
+
# it is evaluated with the +value+ as an argument and checks for a response
|
36
|
+
# other than +nil+ or +false+.
|
37
|
+
#
|
38
|
+
# Searched attributes first check if the +object+ responds to the attribute
|
39
|
+
# so +NoMethodError+ is never thrown if an attribute doesn't exist.
|
40
|
+
#
|
41
|
+
# query = finder(name: 'bob') # => proc { |user| user.name == 'bob' } # pseudo code
|
31
42
|
# query User.new(name: 'steve') # => false
|
32
43
|
# query User.new(name: 'bob') # => true
|
33
44
|
#
|
34
45
|
# users.find(&query) # => #<User @name='bob'>
|
46
|
+
#
|
47
|
+
# users.find &finder(missing: 'value') # => nil
|
35
48
|
def finder(search)
|
36
49
|
Proc.new do |object|
|
37
50
|
search.all? do |attribute, expected|
|
@@ -1,8 +1,10 @@
|
|
1
1
|
class QueryableArray < Array
|
2
|
+
# Makes <tt>[search_hash]</tt> and <tt>[[search_hash]]</tt> behave as an alias
|
3
|
+
# for +find_by+ and +find_all+ respectively
|
2
4
|
module Shorthand
|
3
|
-
# If +key+ is a +Hash+ or an +Array+ containing
|
5
|
+
# If +key+ is a +Hash+, +Proc+, or an +Array+ containing a +Hash+ or +Proc+
|
4
6
|
# then it acts like an alias for +find_by+ or +find_all+ respectively. It
|
5
|
-
#
|
7
|
+
# delegates the call to +super+ in all other cases.
|
6
8
|
#
|
7
9
|
# pages = QueryableArray.new Page.all
|
8
10
|
#
|
@@ -12,9 +14,11 @@ class QueryableArray < Array
|
|
12
14
|
#
|
13
15
|
# pages[[uri: '/']] # => [#<Page @uri='/' @name='Home'>]
|
14
16
|
# pages[[uri: '/', name: 'Typo']] # => []
|
17
|
+
#
|
18
|
+
# pages[uri: proc { |uri| uri.count('/') > 1 }] # => #<Page @uri='/users/bob' @name='Bob'>
|
15
19
|
def [](key)
|
16
20
|
# Try to handle numeric indexes, ranges, and anything else that is
|
17
|
-
# natively supported by Array first
|
21
|
+
# natively supported by +Array+ first
|
18
22
|
super
|
19
23
|
rescue TypeError => error
|
20
24
|
method, key = key.is_a?(Array) ? [:find_all, key.first] : [:find_by, key]
|
@@ -95,6 +95,11 @@ describe QueryableArray do
|
|
95
95
|
collection.respond_to?(:find_all_by).must_equal true
|
96
96
|
collection.find_all_by(:uri => 'page_1').must_equal collection.find_all(:uri => 'page_1')
|
97
97
|
end
|
98
|
+
|
99
|
+
it 'should return a QueryableArray with the same default_finders' do
|
100
|
+
collection.find_all(:uri => 'page_1').default_finders.must_equal collection.default_finders
|
101
|
+
collection.default_finders.wont_be_nil
|
102
|
+
end
|
98
103
|
end
|
99
104
|
|
100
105
|
describe :finder? do
|
@@ -163,6 +168,11 @@ describe QueryableArray do
|
|
163
168
|
collection.pAgE_1?.must_equal true
|
164
169
|
collection.missing?.must_equal false
|
165
170
|
end
|
171
|
+
|
172
|
+
it 'should be case insensitive' do
|
173
|
+
collection.fInD_By_name('PAGE_1').must_equal pages[0]
|
174
|
+
collection.fInD_ALL_By_name('PAGE_1').must_equal [pages[0]]
|
175
|
+
end
|
166
176
|
end
|
167
177
|
|
168
178
|
describe :respond_to_missing? do
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: queryable_array
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.1
|
5
5
|
prerelease:
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2013-01-
|
12
|
+
date: 2013-01-29 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: respond_to_missing
|