queryable_array 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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