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 ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
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,11 @@
1
+ class QueryableArray < Array
2
+ module Version
3
+ MAJOR = 0
4
+ MINOR = 0
5
+ PATCH = 0
6
+
7
+ def self.to_s
8
+ [MAJOR, MINOR, PATCH].join('.')
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,6 @@
1
+ $: << File.expand_path('../../lib', __FILE__)
2
+
3
+ require 'minitest/spec'
4
+ require 'turn/autorun'
5
+
6
+ Turn.config.format = :dot
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