queryable_array 0.0.0 → 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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