queryable_array 0.0.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.
- data/Gemfile +2 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +124 -0
- data/Rakefile +20 -0
- data/lib/queryable_array/default_finder.rb +49 -0
- data/lib/queryable_array/dot_notation.rb +39 -0
- data/lib/queryable_array/dynamic_finder.rb +50 -0
- data/lib/queryable_array/queryable.rb +44 -0
- data/lib/queryable_array/shorthand.rb +24 -0
- data/lib/queryable_array/version.rb +11 -0
- data/lib/queryable_array.rb +16 -0
- data/test/queryable_array_test.rb +187 -0
- data/test/test_helper.rb +6 -0
- metadata +141 -0
data/Gemfile
ADDED
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2013 Sean Huber - shuber@huberry.com
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,124 @@
|
|
1
|
+
= queryable_array
|
2
|
+
|
3
|
+
{<img src="https://secure.travis-ci.org/shuber/queryable_array.png"/>}[http://travis-ci.org/shuber/queryable_array]
|
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.0"/>}[http://rubygems.org/gems/queryable_array]
|
6
|
+
{<img src="https://codeclimate.com/badge.png" />}[https://codeclimate.com/github/shuber/queryable_array]
|
7
|
+
|
8
|
+
A +QueryableArray+ inherits from +Array+ and is intended to store a group of
|
9
|
+
objects which share the same attributes allowing them to be searched. It
|
10
|
+
overrides <tt>[]</tt>, +find_all+ and +method_missing+ to provide a simplified DSL
|
11
|
+
for looking up objects by querying their attributes.
|
12
|
+
|
13
|
+
View the full documentation over at rubydoc.info[http://rubydoc.info/github/shuber/queryable_array/frames].
|
14
|
+
|
15
|
+
== Installation
|
16
|
+
|
17
|
+
gem install queryable_array
|
18
|
+
|
19
|
+
|
20
|
+
== Usage
|
21
|
+
|
22
|
+
=== Basic
|
23
|
+
|
24
|
+
Initialize the +QueryableArray+ with a collection of objects e.g. +Page+ objects from a database
|
25
|
+
|
26
|
+
pages = QueryableArray.new Page.all
|
27
|
+
|
28
|
+
The +pages+ object can then be queried by passing a search hash to the +[]+ method
|
29
|
+
|
30
|
+
pages[uri: '/'] # => #<Page @uri='/' @name='Home'>
|
31
|
+
pages[name: 'About'] # => #<Page @uri='/about' @name='About'>
|
32
|
+
pages[uri: '/', name: 'Home'] # => #<Page @uri='/' @name='Home'>
|
33
|
+
pages[uri: '/', name: 'Mismatch'] # => nil
|
34
|
+
|
35
|
+
Notice that it only returns the first matching object or nil if one is not found. If you'd like to find
|
36
|
+
all matching objects, simply wrap your search hash in an array
|
37
|
+
|
38
|
+
pages[[published: true]] # => [#<Page @uri='/' @name='Home' @published=true>, #<Page @uri='/about' @name='About' @published=true>, ...]
|
39
|
+
pages[[uri: '/missing']] # => []
|
40
|
+
|
41
|
+
Attributes may also be searched by regular expressions
|
42
|
+
|
43
|
+
pages[name: /home/i] # => #<Page @uri='/' @name='Home'>
|
44
|
+
pages[[uri: /users/]] # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
|
45
|
+
|
46
|
+
The methods +find_by+ and +find_all+ behave as aliases for <tt>[search_hash]</tt> and <tt>[[search_hash]]</tt> respectively
|
47
|
+
|
48
|
+
pages.find_by(name: 'Home') # => #<Page @uri='/' @name='Home'>
|
49
|
+
pages.find_by(name: 'Missing') # => nil
|
50
|
+
pages.find_all(uri: /users/) # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
|
51
|
+
|
52
|
+
The existing block form for those methods work as well
|
53
|
+
|
54
|
+
pages.find_all { |page| page.uri =~ /users/ } # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
|
55
|
+
|
56
|
+
A +Proc+ object may be passed to <tt>[]</tt> as well
|
57
|
+
|
58
|
+
pages[uri: proc { |uri| uri.split('/').size > 1 }] # => #<Page @uri='/users/bob' @name='Bob'>
|
59
|
+
pages[proc { |page| page.uri == '/' }] # => #<Page @uri='/' @name='Home'>
|
60
|
+
|
61
|
+
Lookups by index or ranges still behave exactly as they do in regular +Array+ objects
|
62
|
+
|
63
|
+
pages[0] # => #<Page @uri='/' @name='Home'>
|
64
|
+
pages[-1] # => #<Page @uri='/zebras' @name='Zebras'>
|
65
|
+
pages[99] # => nil
|
66
|
+
pages[0..1] # => [#<Page @uri='/' @name='Home'>, #<Page @uri='/about' @name='About'>]
|
67
|
+
|
68
|
+
|
69
|
+
=== Default finders
|
70
|
+
|
71
|
+
A +QueryableArray+ object can be initialized with a +default_finder+ to make lookups even simpler
|
72
|
+
|
73
|
+
pages = QueryableArray.new Page.all, :uri
|
74
|
+
|
75
|
+
Now the +pages+ object can be searched easily by +uri+
|
76
|
+
|
77
|
+
pages['/'] # => #<Page @uri='/' @name='Home'>
|
78
|
+
pages['/about'] # => #<Page @uri='/about' @name='About'>
|
79
|
+
pages['/missing'] # => nil
|
80
|
+
|
81
|
+
You can even specify multiple +default_finders+
|
82
|
+
|
83
|
+
pages = QueryableArray.new Page.all, [:uri, :name]
|
84
|
+
|
85
|
+
pages['/about'] # => #<Page @uri='/about' @name='About'>
|
86
|
+
pages['About'] # => #<Page @uri='/about' @name='About'>
|
87
|
+
pages[/home/i] # => #<Page @uri='/' @name='Home'>
|
88
|
+
|
89
|
+
Wrapping your search inside an array still returns all matches
|
90
|
+
|
91
|
+
pages[[/users/]] # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
|
92
|
+
|
93
|
+
|
94
|
+
=== Dynamic attribute-based finders
|
95
|
+
|
96
|
+
<tt>QueryableArray#method_missing</tt> allows you to lookup objects using a notation like the +ActiveRecord+ dynamic finders
|
97
|
+
|
98
|
+
pages.find_by_uri('/') # => #<Page @uri='/' @name='Home'>
|
99
|
+
pages.find_by_uri_and_name('/', 'Home') # => #<Page @uri='/' @name='Home'>
|
100
|
+
pages.find_by_uri('/missing') # => nil
|
101
|
+
|
102
|
+
pages.find_all_by_uri('/') # => [#<Page @uri='/' @name='Home'>]
|
103
|
+
pages.find_all_by_uri(/users/) # => [#<Page @uri='/users/bob' @name='Bob'>, #<Page @uri='/users/steve' @name='Steve'>, ...]
|
104
|
+
|
105
|
+
|
106
|
+
=== Dot notation finders
|
107
|
+
|
108
|
+
If any +default_finders+ are defined you may even use dot notation to lookup objects by those attributes
|
109
|
+
|
110
|
+
pages = QueryableArray.new Page.all, :name
|
111
|
+
|
112
|
+
pages.sitemap # => #<Page @uri='/sitemap' @name='Sitemap'>
|
113
|
+
pages.missing # => NoMethodError
|
114
|
+
QueryableArray.new.missing # => NoMethodError
|
115
|
+
|
116
|
+
Calling <tt>pages.sitemap</tt> behaves the same as <tt>pages[/sitemap/i]</tt>
|
117
|
+
|
118
|
+
To perform a case-sensitive search, simply append a <tt>!</tt> to the end of your method call
|
119
|
+
e.g. <tt>pages.sitemap!</tt> which calls <tt>pages['sitemap']</tt>
|
120
|
+
|
121
|
+
You may also query to see if a match exists by appending a <tt>?</tt> to your search
|
122
|
+
|
123
|
+
pages.sitemap? # => true
|
124
|
+
pages.missing? # => false
|
data/Rakefile
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
require 'rake/testtask'
|
2
|
+
require 'rdoc/task'
|
3
|
+
|
4
|
+
desc 'Default: run unit tests.'
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
desc 'Test the queryable_array gem.'
|
8
|
+
Rake::TestTask.new(:test) do |t|
|
9
|
+
t.libs.push 'lib'
|
10
|
+
t.pattern = 'test/**/*_test.rb'
|
11
|
+
end
|
12
|
+
|
13
|
+
desc 'Generate documentation for the queryable_array gem.'
|
14
|
+
Rake::RDocTask.new(:rdoc) do |rdoc|
|
15
|
+
rdoc.rdoc_dir = 'rdoc'
|
16
|
+
rdoc.title = 'queryable_array'
|
17
|
+
rdoc.options << '--line-numbers' << '--inline-source'
|
18
|
+
rdoc.rdoc_files.include('README*')
|
19
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
20
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
class QueryableArray < Array
|
2
|
+
module DefaultFinder
|
3
|
+
attr_accessor :default_finders
|
4
|
+
|
5
|
+
# Accepts an initial +array+ which defaults to +[]+. An optional +default_finders+
|
6
|
+
# may also be specified as the second argument which is used in +QueryableArray#[]+
|
7
|
+
# for quick lookups. It defaults to +nil+ which disables this behavior. See the
|
8
|
+
# +QueryableArray#[]+ method for more documentation.
|
9
|
+
def initialize(array = [], default_finders = nil)
|
10
|
+
super array
|
11
|
+
self.default_finders = Array(default_finders)
|
12
|
+
end
|
13
|
+
|
14
|
+
# If +default_finders+ has been set and +key+ is not a +Fixnum+ then it
|
15
|
+
# loops thru each +default_finders+ and returns the first matching result
|
16
|
+
# of +find_by(finder => key)+ or +find_all(finder => key.first)+ if +key+
|
17
|
+
# is an +Array+. If +key+ is already a +Hash+ or an +Array+ containing one
|
18
|
+
# then it acts like an alias for +find_by+ or +find_all+ respectively. It
|
19
|
+
# behaves exactly like its superclass +Array+ in all other cases.
|
20
|
+
#
|
21
|
+
# pages = QueryableArray.new(Page.all, [:uri, :name])
|
22
|
+
#
|
23
|
+
# pages['/'] # => #<Page @uri='/'>
|
24
|
+
# pages['Home'] # => #<Page @name='Home'>
|
25
|
+
# pages[/home/i] # => #<Page @name='Home'>
|
26
|
+
# pages['missing'] # => nil
|
27
|
+
def [](key)
|
28
|
+
super
|
29
|
+
rescue TypeError => error
|
30
|
+
if default_finders.empty?
|
31
|
+
raise error
|
32
|
+
else
|
33
|
+
method, key = key.is_a?(Array) ? [:find_all, key.first] : [:find_by, key]
|
34
|
+
send method, &query(key)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def query(search)
|
39
|
+
Proc.new do |object|
|
40
|
+
proc = search.respond_to?(:call) ? search : Proc.new do |object|
|
41
|
+
default_finders.any? do |attribute|
|
42
|
+
finder(attribute => search).call object
|
43
|
+
end
|
44
|
+
end
|
45
|
+
proc.call object
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'queryable_array/default_finder'
|
2
|
+
|
3
|
+
class QueryableArray < Array
|
4
|
+
module DotNotation
|
5
|
+
def self.included(base)
|
6
|
+
base.send :include, DefaultFinder
|
7
|
+
end
|
8
|
+
|
9
|
+
# If +method_name+ does not have a <tt>!</tt> or <tt>?</tt> suffix then +self[/#{method_name}/i]+
|
10
|
+
# is returned. If it returns +nil+ or raises +TypeError+ (no +default_finders+) then +super+ is returned.
|
11
|
+
#
|
12
|
+
# If +method_name+ ends in a <tt>!</tt> then +self[method_name]+ (without the <tt>!</tt>) is returned. If +method_name+
|
13
|
+
# ends with a <tt>?</tt> then a boolean is returned determining whether or not a match was found.
|
14
|
+
#
|
15
|
+
# users = QueryableArray.new User.all, :username
|
16
|
+
#
|
17
|
+
# users.bob # => #<User @name='bob'>
|
18
|
+
# users.BOB # => #<User @name='bob'>
|
19
|
+
# users.missing # => NoMethodError
|
20
|
+
# QueryableArray.new.missing # => NoMethodError
|
21
|
+
def method_missing(method_name, *arguments)
|
22
|
+
if method_name.to_s =~ /^(.+?)([\!\?])?$/
|
23
|
+
search = $2 == '!' ? $1 : /#{$1}/i
|
24
|
+
value = begin
|
25
|
+
self[search]
|
26
|
+
rescue TypeError
|
27
|
+
nil
|
28
|
+
end
|
29
|
+
$2 == '?' ? !!value : (value || super)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def respond_to_missing?(method_name, include_super)
|
34
|
+
!!(method_name.to_s =~/\?$/ || super || send(method_name))
|
35
|
+
rescue NoMethodError
|
36
|
+
false
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
class QueryableArray < Array
|
2
|
+
module DynamicFinder
|
3
|
+
def self.included(base)
|
4
|
+
base.send :alias_method, :find_all_by, :find_all
|
5
|
+
end
|
6
|
+
|
7
|
+
# Determines if the +method_name+ passed to it can be parsed into a search hash
|
8
|
+
# for +finder+.
|
9
|
+
#
|
10
|
+
# finder? :find_by_name # => true
|
11
|
+
# finder? :find_by_first_name_and_last_name # => true
|
12
|
+
# finder? :find_all_by_last_name # => true
|
13
|
+
# finder? :find_all_by_last_name_and_city # => true
|
14
|
+
# finder? :find_by_name_or_age # => false
|
15
|
+
# finder? :find_first_by_name # => false
|
16
|
+
# finder? :find_name # => false
|
17
|
+
# finder? :some_method # => false
|
18
|
+
def finder?(method_name)
|
19
|
+
if match = method_name.to_s.match(/^(find_(by|all_by))_(.+?)([\?\!])?$/i)
|
20
|
+
keys = [:method_name, :prefix, :type, :attributes, :suffix]
|
21
|
+
Hash[keys.zip match.to_a]
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# If +method_name+ is a +finder?+ then it creates a search hash by zipping the
|
26
|
+
# names of attributes parsed from +method_name+ as keys and +arguments+ as expected
|
27
|
+
# values. The search hash is then passed to +find_all+ or +find_by+ depending
|
28
|
+
# on what type of method was called.
|
29
|
+
#
|
30
|
+
# users = QueryableArray.new User.all, :username
|
31
|
+
#
|
32
|
+
# users.find_by_name 'bob' # => #<User @name='bob'>
|
33
|
+
# users.find_by_name_and_age 'jim', 23 # => #<User @name='jim' @age=23>
|
34
|
+
# users.find_all_by_age 27 # => [#<User @age=27>, #<User @age=27>, ...]
|
35
|
+
def method_missing(method_name, *arguments)
|
36
|
+
if query = finder?(method_name)
|
37
|
+
search = Hash[query[:attributes].split('_and_').zip(arguments)]
|
38
|
+
send "find_#{query[:type]}", search
|
39
|
+
else
|
40
|
+
super
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Determines if +method_name+ is a +finder?+ and delegates the call to its
|
45
|
+
# superclass +Array+ if it's not.
|
46
|
+
def respond_to_missing?(method_name, include_super)
|
47
|
+
!!finder?(method_name) || super
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
class QueryableArray < Array
|
2
|
+
module Queryable
|
3
|
+
# Returns a QueryableArray of objects matching +search+ criteria. When a +block+
|
4
|
+
# is specified, it behaves exactly like +Enumerable#find_all+. Otherwise the
|
5
|
+
# +search+ hash is converted into a +finder+ proc and passed as the block
|
6
|
+
# argument to +Enumerable#find_all+.
|
7
|
+
#
|
8
|
+
# users.find_all(age: 30) # => [#<User @age=30>, #<User @age=30>, ...]
|
9
|
+
# users.find_all(name: 'missing') # => []
|
10
|
+
# users.find_all { |user| user.age < 30 } # => [#<User @age=22>, #<User @age=26>, ...]
|
11
|
+
def find_all(search = {}, &block)
|
12
|
+
block = finder search unless block_given?
|
13
|
+
self.class.new super(&block)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Behaves exactly like +find_all+ but only returns the first match. If no match
|
17
|
+
# is found then +nil+ is returned.
|
18
|
+
#
|
19
|
+
# users.find_by(age: 25) # => #<User @age=25>
|
20
|
+
# users.find_by(name: 'missing') # => nil
|
21
|
+
def find_by(search = {}, &block)
|
22
|
+
block = finder search unless block_given?
|
23
|
+
find(&block)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Accepts a +search+ hash and returns a +Proc+ which determines if all of an
|
27
|
+
# object's attributes match their expected search values. It can be used as
|
28
|
+
# the block arguments for +find+, +find_by+ and +find_all+.
|
29
|
+
#
|
30
|
+
# query = finder name: 'bob' # => proc { |user| user.name == 'bob' }
|
31
|
+
# query User.new(name: 'steve') # => false
|
32
|
+
# query User.new(name: 'bob') # => true
|
33
|
+
#
|
34
|
+
# users.find(&query) # => #<User @name='bob'>
|
35
|
+
def finder(search)
|
36
|
+
Proc.new do |object|
|
37
|
+
search.all? do |attribute, expected|
|
38
|
+
value = object.send attribute if object.respond_to?(attribute)
|
39
|
+
expected == value || expected === value || (expected.respond_to?(:call) && expected.call(value))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
class QueryableArray < Array
|
2
|
+
module Shorthand
|
3
|
+
# If +key+ is a +Hash+ or an +Array+ containing one
|
4
|
+
# then it acts like an alias for +find_by+ or +find_all+ respectively. It
|
5
|
+
# behaves exactly like its superclass +Array+ in all other cases.
|
6
|
+
#
|
7
|
+
# pages = QueryableArray.new Page.all
|
8
|
+
#
|
9
|
+
# pages[uri: '/'] # => #<Page @uri='/' @name='Home'>
|
10
|
+
# pages[uri: '/', name: 'Home'] # => #<Page @uri='/' @name='Home'>
|
11
|
+
# pages[uri: '/', name: 'Typo'] # => nil
|
12
|
+
#
|
13
|
+
# pages[[uri: '/']] # => [#<Page @uri='/' @name='Home'>]
|
14
|
+
# pages[[uri: '/', name: 'Typo']] # => []
|
15
|
+
def [](key)
|
16
|
+
# Try to handle numeric indexes, ranges, and anything else that is
|
17
|
+
# natively supported by Array first
|
18
|
+
super
|
19
|
+
rescue TypeError => error
|
20
|
+
method, key = key.is_a?(Array) ? [:find_all, key.first] : [:find_by, key]
|
21
|
+
key.is_a?(Hash) ? send(method, key) : raise(error)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
require 'respond_to_missing'
|
2
|
+
require 'queryable_array/dot_notation'
|
3
|
+
require 'queryable_array/dynamic_finder'
|
4
|
+
require 'queryable_array/queryable'
|
5
|
+
require 'queryable_array/shorthand'
|
6
|
+
|
7
|
+
# A +QueryableArray+ inherits from +Array+ and is intended to store a group of
|
8
|
+
# objects which share the same attributes allowing them to be searched. It
|
9
|
+
# overrides +[]+, +find_all+ and +method_missing+ to provide a simplified DSL
|
10
|
+
# for looking up objects by querying their attributes.
|
11
|
+
class QueryableArray < Array
|
12
|
+
include Queryable
|
13
|
+
include Shorthand
|
14
|
+
include DotNotation
|
15
|
+
include DynamicFinder
|
16
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
require File.expand_path('../test_helper', __FILE__)
|
2
|
+
require 'queryable_array'
|
3
|
+
|
4
|
+
describe QueryableArray do
|
5
|
+
let(:page) { Struct.new(:uri, :name) }
|
6
|
+
let(:pages) { (1..3).inject([]) { |pages, index| pages << page.new("page_#{index}", "PAGE_#{index}") } }
|
7
|
+
let(:collection) { QueryableArray.new pages, [:uri, :name] }
|
8
|
+
|
9
|
+
describe :[] do
|
10
|
+
it 'should accept Fixnum arguments' do
|
11
|
+
collection[0].must_equal pages[0]
|
12
|
+
collection[99].must_be_nil
|
13
|
+
end
|
14
|
+
|
15
|
+
it 'should lookup objects by default_finders' do
|
16
|
+
collection['page_1'].must_equal pages[0]
|
17
|
+
collection['PAGE_1'].must_equal pages[0]
|
18
|
+
collection['page_99'].must_be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should lookup objects by regex' do
|
22
|
+
collection[/page_1/].must_equal pages[0]
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'should lookup objects by hash' do
|
26
|
+
collection[:uri => 'page_1'].must_equal pages[0]
|
27
|
+
collection[:uri => 'page_1', :name => 'PAGE_1'].must_equal pages[0]
|
28
|
+
collection[:uri => 'page_1', :name => 'INVALID'].must_equal nil
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'should not accept shorthand searches if default_finders is nil' do
|
32
|
+
proc { QueryableArray.new(pages)['page_1'] }.must_raise TypeError
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'should accept an array to return all matches' do
|
36
|
+
collection[['page_1']].must_equal [pages[0]]
|
37
|
+
collection[[/page/]].must_equal pages
|
38
|
+
collection[['missing']].must_equal []
|
39
|
+
collection[[:uri => 'page_1']].must_equal [pages[0]]
|
40
|
+
collection[[:uri => 'page_1', :name => 'PAGE_1']].must_equal [pages[0]]
|
41
|
+
collection[[:uri => /page/]].must_equal pages
|
42
|
+
collection[[:uri => 'page_1', :name => 'INVALID']].must_equal []
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'should check all attributes before returning matches' do
|
46
|
+
struct = Struct.new(:first_name, :last_name)
|
47
|
+
collection = QueryableArray.new [struct.new('steve', 'hudson'), struct.new('hudson', 'jones')], [:first_name, :last_name]
|
48
|
+
collection[['hudson']].must_equal collection
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'should accept a proc' do
|
52
|
+
collection[:name => proc { |name| $1.to_i == 2 if name =~ /.+?(\d+)$/ }].must_equal pages[1]
|
53
|
+
collection[proc { |page| page.uri == 'page_1' }].must_equal pages[0]
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'should accept callable objects' do
|
57
|
+
callable = Class.new { def self.call(object); object.to_s == 'PAGE_1'; end }
|
58
|
+
collection[:name => callable].must_equal pages[0]
|
59
|
+
collection[callable].must_be_nil
|
60
|
+
pages[0].instance_eval { def to_s; 'PAGE_1'; end }
|
61
|
+
collection[callable].must_equal pages[0]
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'should not throw NoMethodError if an attribute does not exist' do
|
65
|
+
collection[:missing => 'test'].must_be_nil
|
66
|
+
collection[[:missing => 'test']].must_equal []
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
describe :find_by do
|
71
|
+
it 'should only return the first match' do
|
72
|
+
struct = Struct.new(:name, :age)
|
73
|
+
collection = QueryableArray.new [struct.new('bob', 23), struct.new('steve', 23)]
|
74
|
+
collection.find_by(:age => 23).must_equal collection[0]
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
describe :find_all do
|
79
|
+
it 'should return a QueryableArray instance' do
|
80
|
+
collection.find_all { |page| page.uri == 'page_1' }.must_be_instance_of QueryableArray
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'should lookup by search Hash' do
|
84
|
+
collection.find_all(:uri => 'page_1').must_equal [pages[0]]
|
85
|
+
collection.find_all(:uri => 'page_1', :name => 'PAGE_1').must_equal [pages[0]]
|
86
|
+
collection.find_all(:uri => 'page_1', :name => 'PAGE_3').must_equal []
|
87
|
+
end
|
88
|
+
|
89
|
+
it 'should lookup by regex matches' do
|
90
|
+
collection.find_all(:uri => /^page_\d$/).must_equal collection
|
91
|
+
collection.find_all(:uri => /^page_1$/).must_equal [pages[0]]
|
92
|
+
end
|
93
|
+
|
94
|
+
it 'should be aliased as find_all_by' do
|
95
|
+
collection.respond_to?(:find_all_by).must_equal true
|
96
|
+
collection.find_all_by(:uri => 'page_1').must_equal collection.find_all(:uri => 'page_1')
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
describe :finder? do
|
101
|
+
it 'should check if a method matches a finder' do
|
102
|
+
collection.finder?('find_by_name').wont_be_nil
|
103
|
+
collection.finder?('find_all_by_name').wont_be_nil
|
104
|
+
collection.finder?('find_by_name_and_uri').wont_be_nil
|
105
|
+
collection.finder?('find_all_by_name_and_uri').wont_be_nil
|
106
|
+
collection.finder?('find_by_nil?').wont_be_nil
|
107
|
+
collection.finder?('find_by_name!').wont_be_nil
|
108
|
+
collection.finder?('find_by').must_be_nil
|
109
|
+
end
|
110
|
+
|
111
|
+
describe 'a match' do
|
112
|
+
let(:match) { collection.finder?('find_all_by_name_and_email_address!') }
|
113
|
+
let(:singular) { collection.finder?('find_by_name') }
|
114
|
+
let(:keys) { [:method_name, :prefix, :type, :attributes, :suffix] }
|
115
|
+
|
116
|
+
it 'should return a Hash object' do
|
117
|
+
match.must_be_instance_of Hash
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'should return results in a specific order' do
|
121
|
+
match.must_equal Hash[keys.zip %w[find_all_by_name_and_email_address! find_all_by all_by name_and_email_address !]]
|
122
|
+
singular.must_equal Hash[keys.zip ['find_by_name', 'find_by', 'by', 'name', nil]]
|
123
|
+
end
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe :method_missing do
|
128
|
+
it 'should pass finder methods to find_by' do
|
129
|
+
pages = collection.find_all_by_name('PAGE_1')
|
130
|
+
pages.size.must_equal 1
|
131
|
+
pages[0].name.must_equal 'PAGE_1'
|
132
|
+
collection.find_by_name('PAGE_1').must_equal pages[0]
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'should allow searches by undefined methods' do
|
136
|
+
collection.find_by_undefined_method('PAGE_1').must_be_nil
|
137
|
+
end
|
138
|
+
|
139
|
+
it 'should allow multiple search attributes' do
|
140
|
+
collection.find_by_name_and_uri('PAGE_1', 'page_1').must_equal pages[0]
|
141
|
+
collection.find_by_name_and_uri('PAGE_1', 'page_3').must_be_nil
|
142
|
+
end
|
143
|
+
|
144
|
+
it 'should allow default finder lookups by method name using regex' do
|
145
|
+
collection.page_1.must_equal pages[0]
|
146
|
+
collection.PAGE_1.must_equal pages[0]
|
147
|
+
collection.pAgE_1.must_equal pages[0]
|
148
|
+
proc { collection.page_99 }.must_raise NoMethodError
|
149
|
+
end
|
150
|
+
|
151
|
+
it 'should allow strict default finder lookups by method name using the raw value' do
|
152
|
+
collection.page_1!.must_equal pages[0]
|
153
|
+
collection.PAGE_1!.must_equal pages[0]
|
154
|
+
proc { collection.pAgE_1! }.must_raise NoMethodError
|
155
|
+
end
|
156
|
+
|
157
|
+
it 'should pass non finder methods to super' do
|
158
|
+
proc { collection.missing }.must_raise NoMethodError
|
159
|
+
end
|
160
|
+
|
161
|
+
it 'should return a boolean when looking up using a ?' do
|
162
|
+
collection.page_1?.must_equal true
|
163
|
+
collection.pAgE_1?.must_equal true
|
164
|
+
collection.missing?.must_equal false
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
describe :respond_to_missing? do
|
169
|
+
it 'should check if the method is a finder' do
|
170
|
+
collection.respond_to?(:find_by_name).must_equal true
|
171
|
+
collection.respond_to?(:find_all_by_name).must_equal true
|
172
|
+
collection.respond_to?(:find_stuff).must_equal false
|
173
|
+
collection.respond_to?(:invalid).must_equal false
|
174
|
+
end
|
175
|
+
|
176
|
+
it 'should check if the method can be handled by DotNotation' do
|
177
|
+
collection.respond_to?(:page_1).must_equal true
|
178
|
+
collection.respond_to?(:PAGE_1).must_equal true
|
179
|
+
collection.respond_to?(:pAgE_1).must_equal true
|
180
|
+
collection.respond_to?(:page_1?).must_equal true
|
181
|
+
collection.respond_to?(:missing?).must_equal true
|
182
|
+
collection.respond_to?(:page_1!).must_equal true
|
183
|
+
collection.respond_to?(:pAgE_1!).must_equal false
|
184
|
+
collection.respond_to?(:missing).must_equal false
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
data/test/test_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,141 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: queryable_array
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Sean Huber
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2013-01-24 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: respond_to_missing
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: '0'
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: '0'
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: minitest
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '0'
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '0'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rake
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ! '>='
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: '0'
|
54
|
+
type: :development
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ! '>='
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
- !ruby/object:Gem::Dependency
|
63
|
+
name: rdoc
|
64
|
+
requirement: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ! '>='
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
70
|
+
type: :development
|
71
|
+
prerelease: false
|
72
|
+
version_requirements: !ruby/object:Gem::Requirement
|
73
|
+
none: false
|
74
|
+
requirements:
|
75
|
+
- - ! '>='
|
76
|
+
- !ruby/object:Gem::Version
|
77
|
+
version: '0'
|
78
|
+
- !ruby/object:Gem::Dependency
|
79
|
+
name: turn
|
80
|
+
requirement: !ruby/object:Gem::Requirement
|
81
|
+
none: false
|
82
|
+
requirements:
|
83
|
+
- - ! '>='
|
84
|
+
- !ruby/object:Gem::Version
|
85
|
+
version: '0'
|
86
|
+
type: :development
|
87
|
+
prerelease: false
|
88
|
+
version_requirements: !ruby/object:Gem::Requirement
|
89
|
+
none: false
|
90
|
+
requirements:
|
91
|
+
- - ! '>='
|
92
|
+
- !ruby/object:Gem::Version
|
93
|
+
version: '0'
|
94
|
+
description: QueryableArray is intended to store a group of objects which share the
|
95
|
+
same attributes allowing them to be searched with a simplified DSL
|
96
|
+
email: shuber@huberry.com
|
97
|
+
executables: []
|
98
|
+
extensions: []
|
99
|
+
extra_rdoc_files: []
|
100
|
+
files:
|
101
|
+
- lib/queryable_array/default_finder.rb
|
102
|
+
- lib/queryable_array/dot_notation.rb
|
103
|
+
- lib/queryable_array/dynamic_finder.rb
|
104
|
+
- lib/queryable_array/queryable.rb
|
105
|
+
- lib/queryable_array/shorthand.rb
|
106
|
+
- lib/queryable_array/version.rb
|
107
|
+
- lib/queryable_array.rb
|
108
|
+
- Gemfile
|
109
|
+
- MIT-LICENSE
|
110
|
+
- Rakefile
|
111
|
+
- README.rdoc
|
112
|
+
- test/queryable_array_test.rb
|
113
|
+
- test/test_helper.rb
|
114
|
+
homepage: http://github.com/shuber/queryable_array
|
115
|
+
licenses: []
|
116
|
+
post_install_message:
|
117
|
+
rdoc_options: []
|
118
|
+
require_paths:
|
119
|
+
- lib
|
120
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ! '>='
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: '0'
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
none: false
|
128
|
+
requirements:
|
129
|
+
- - ! '>='
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
requirements: []
|
133
|
+
rubyforge_project:
|
134
|
+
rubygems_version: 1.8.23
|
135
|
+
signing_key:
|
136
|
+
specification_version: 3
|
137
|
+
summary: Provides a simplified DSL allowing arrays of objects to be searched by their
|
138
|
+
attributes
|
139
|
+
test_files:
|
140
|
+
- test/queryable_array_test.rb
|
141
|
+
- test/test_helper.rb
|