queryable_array 0.0.0 → 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.0"/>}[http://rubygems.org/gems/queryable_array]
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 +[]+ method
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
@@ -1,5 +1,9 @@
1
1
  require 'rake/testtask'
2
- require 'rdoc/task'
2
+ begin
3
+ require 'rdoc/task'
4
+ rescue LoadError
5
+ require 'rake/rdoctask'
6
+ end
3
7
 
4
8
  desc 'Default: run unit tests.'
5
9
  task :default => :test
@@ -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+ 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.
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 +self[/#{method_name}/i]+
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 +self[method_name]+ (without the <tt>!</tt>) is returned. If +method_name+
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 # => #<User @name='bob'>
18
- # users.BOB # => #<User @name='bob'>
19
- # users.missing # => NoMethodError
20
- # QueryableArray.new.missing # => NoMethodError
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 =~/\?$/ || super || send(method_name))
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 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+.
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
- self.class.new super(&block)
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 search values. It can be used as
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
- # query = finder name: 'bob' # => proc { |user| user.name == 'bob' }
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 one
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
- # behaves exactly like its superclass +Array+ in all other cases.
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]
@@ -2,7 +2,7 @@ class QueryableArray < Array
2
2
  module Version
3
3
  MAJOR = 0
4
4
  MINOR = 0
5
- PATCH = 0
5
+ PATCH = 1
6
6
 
7
7
  def self.to_s
8
8
  [MAJOR, MINOR, PATCH].join('.')
@@ -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.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-24 00:00:00.000000000 Z
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