queryable_array 0.0.0 → 0.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/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
|