active_collection 0.2.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/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Martin Emde
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,8 @@
1
+ = ActiveCollection
2
+
3
+ Lazy-loaded Array-like collections of records.
4
+ Compatible with will_paginate.
5
+
6
+ == Copyright
7
+
8
+ Copyright (c) 2009 Martin Emde. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,52 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "active_collection"
8
+ gem.summary = %Q{Lazy-loaded array of records}
9
+ gem.description = %Q{Lazy-loaded array of records}
10
+ gem.email = "martin.emde@gmail.com"
11
+ gem.homepage = "http://github.com/martinemde/active_collection"
12
+ gem.authors = ["Martin Emde"]
13
+ gem.add_development_dependency "rspec"
14
+ gem.rubyforge_project = "collection"
15
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
16
+ end
17
+ Jeweler::RubyforgeTasks.new do |rubyforge|
18
+ rubyforge.doc_task = "rdoc"
19
+ end
20
+ rescue LoadError
21
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
22
+ end
23
+
24
+ require 'spec/rake/spectask'
25
+ Spec::Rake::SpecTask.new(:spec) do |spec|
26
+ spec.libs << 'lib' << 'spec'
27
+ spec.spec_files = FileList['spec/**/*_spec.rb']
28
+ end
29
+
30
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
31
+ spec.libs << 'lib' << 'spec'
32
+ spec.pattern = 'spec/**/*_spec.rb'
33
+ spec.rcov = true
34
+ end
35
+
36
+ task :spec => :check_dependencies
37
+
38
+ task :default => :spec
39
+
40
+ require 'rake/rdoctask'
41
+ Rake::RDocTask.new do |rdoc|
42
+ if File.exist?('VERSION')
43
+ version = File.read('VERSION')
44
+ else
45
+ version = ""
46
+ end
47
+
48
+ rdoc.rdoc_dir = 'rdoc'
49
+ rdoc.title = "active_collection #{version}"
50
+ rdoc.rdoc_files.include('README*')
51
+ rdoc.rdoc_files.include('lib/**/*.rb')
52
+ end
data/VERSION.yml ADDED
@@ -0,0 +1,4 @@
1
+ ---
2
+ :major: 0
3
+ :minor: 2
4
+ :patch: 0
@@ -0,0 +1,62 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{active_collection}
8
+ s.version = "0.2.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Martin Emde"]
12
+ s.date = %q{2009-09-14}
13
+ s.description = %q{Lazy-loaded array of records}
14
+ s.email = %q{martin.emde@gmail.com}
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".document",
21
+ ".gitignore",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "VERSION.yml",
26
+ "active_collection.gemspec",
27
+ "lib/active_collection.rb",
28
+ "lib/active_collection/base.rb",
29
+ "lib/active_collection/includes.rb",
30
+ "lib/active_collection/order.rb",
31
+ "lib/active_collection/pagination.rb",
32
+ "lib/active_collection/scope.rb",
33
+ "spec/active_collection_spec.rb",
34
+ "spec/pagination_spec.rb",
35
+ "spec/spec.opts",
36
+ "spec/spec_helper.rb"
37
+ ]
38
+ s.homepage = %q{http://github.com/martinemde/active_collection}
39
+ s.rdoc_options = ["--charset=UTF-8"]
40
+ s.require_paths = ["lib"]
41
+ s.rubyforge_project = %q{collection}
42
+ s.rubygems_version = %q{1.3.5}
43
+ s.summary = %q{Lazy-loaded array of records}
44
+ s.test_files = [
45
+ "spec/active_collection_spec.rb",
46
+ "spec/pagination_spec.rb",
47
+ "spec/spec_helper.rb"
48
+ ]
49
+
50
+ if s.respond_to? :specification_version then
51
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
52
+ s.specification_version = 3
53
+
54
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
55
+ s.add_development_dependency(%q<rspec>, [">= 0"])
56
+ else
57
+ s.add_dependency(%q<rspec>, [">= 0"])
58
+ end
59
+ else
60
+ s.add_dependency(%q<rspec>, [">= 0"])
61
+ end
62
+ end
@@ -0,0 +1,14 @@
1
+ activesupport_path = "#{File.dirname(__FILE__)}/../../activesupport/lib"
2
+ $:.unshift(activesupport_path) if File.directory?(activesupport_path)
3
+ require 'active_support'
4
+
5
+ module ActiveCollection
6
+ autoload :Base, 'active_collection/base'
7
+ autoload :Scope, 'active_collection/scope'
8
+ autoload :Order, 'active_collection/order'
9
+ autoload :Includes, 'active_collection/includes'
10
+ autoload :Pagination, 'active_collection/pagination'
11
+
12
+ Base
13
+ end
14
+
@@ -0,0 +1,213 @@
1
+ # Lazy-loaded collection of records
2
+ # Behaves like an Array or Hash (where bang methods alter self)
3
+ module ActiveCollection
4
+ # Raised when a mutating method is called on an already loaded collection.
5
+ class AlreadyLoadedError < StandardError #:nodoc:
6
+ end
7
+
8
+ class Base
9
+ #instance_methods.each do |m|
10
+ # unless m =~ /(^__|^proxy_)/ || %w[should should_not nil? send dup extend inspect object_id].include?(m)
11
+ # undef_method m
12
+ # end
13
+ #end
14
+
15
+ include Enumerable
16
+
17
+ attr_reader :params
18
+
19
+ # Create a Collection by passing the important query params from
20
+ # the controller.
21
+ #
22
+ # Example:
23
+ #
24
+ # BeerCollection.new(params.only("q","page"))
25
+ #
26
+ # If any :page parameter is passed, nil or not, the assumption will be that
27
+ # the collection should be a paged collection and the current_page will
28
+ # default to 1.
29
+ def initialize(params = {})
30
+ @params = params.symbolize_keys
31
+ end
32
+
33
+ alias_method :proxy_respond_to?, :respond_to?
34
+
35
+ # Does the ActiveCollection or it's target collection respond to method?
36
+ def respond_to?(*args)
37
+ proxy_respond_to?(*args) || collection.respond_to?(*args)
38
+ end
39
+
40
+ def self.model_class
41
+ @model_class ||= name.sub(/Collection$/,'').constantize
42
+ end
43
+
44
+ def model_class
45
+ self.class.model_class
46
+ end
47
+
48
+ def self.table_name
49
+ model_class.table_name
50
+ end
51
+
52
+ def table_name
53
+ self.class.table_name
54
+ end
55
+
56
+ def self.human_name
57
+ table_name.gsub(/_/,' ')
58
+ end
59
+
60
+ def human_name
61
+ self.class.human_name
62
+ end
63
+
64
+ # Forwards <tt>===</tt> explicitly to the collection because the instance method
65
+ # removal above doesn't catch it. Loads the collection if needed.
66
+ def ===(other)
67
+ other === collection
68
+ end
69
+
70
+ def send(method, *args)
71
+ if proxy_respond_to?(method)
72
+ super
73
+ else
74
+ collection.send(method, *args)
75
+ end
76
+ end
77
+
78
+ # Turn the params into a hash suitable for passing the collection directly as an arg to a named path.
79
+ def to_param
80
+ params.empty?? nil : params.to_param
81
+ end
82
+
83
+ def as_data_hash
84
+ data_hash = { "collection" => collection.as_json }
85
+ data_hash["total_entries"] = total_entries
86
+ data_hash
87
+ end
88
+
89
+ def to_xml(options = {})
90
+ collect
91
+ options[:indent] ||= 2
92
+ xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
93
+ xml.instruct! unless options[:skip_instruct]
94
+ xml.tag!(table_name) do
95
+ xml.total_entries(total_entries, :type => "integer")
96
+ xml.collection(:type => "array") do
97
+ collection.each do |item|
98
+ item.to_xml(:indent => options[:indent], :builder => xml, :skip_instruct => true)
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ def as_json(options = nil)
105
+ {table_name => as_data_hash}.as_json(options)
106
+ end
107
+
108
+ # Implements Enumerable
109
+ def each(&block)
110
+ collection.each(&block)
111
+ end
112
+
113
+ # Grab the raw collection.
114
+ def all
115
+ collection
116
+ end
117
+
118
+ # The emptiness of the collection (limited by query and pagination)
119
+ def empty?
120
+ size.zero?
121
+ end
122
+
123
+ # The size of the collection (limited by query and pagination)
124
+ #
125
+ # It will avoid using a count query if the collection is already loaded.
126
+ def size
127
+ loaded?? collection.size : total_entries
128
+ end
129
+
130
+ # Always returns the total count regardless of pagination.
131
+ def total_entries
132
+ @total_entries ||= load_count
133
+ end
134
+
135
+ # The size of the collection (limited by query and pagination)
136
+ #
137
+ # Similar to ActiveRecord associations, length will always load the collection.
138
+ def length
139
+ collection.size
140
+ end
141
+
142
+ # true if the collection data has been loaded
143
+ def loaded?
144
+ !!@collection
145
+ end
146
+
147
+ protected
148
+
149
+ # Pass methods on to the collection.
150
+ def method_missing(method, *args)
151
+ if Array.method_defined?(method) && !Object.method_defined?(method)
152
+ raise "#{method} received with #{args.join(', ')}"
153
+ if block_given?
154
+ collection.send(method, *args) { |*block_args| yield(*block_args) }
155
+ else
156
+ collection.send(method, *args)
157
+ end
158
+ else
159
+ super
160
+ #message = "undefined method `#{method.to_s}' for \"#{collection}\":#{collection.class.to_s}"
161
+ #raise NoMethodError, message
162
+ end
163
+ end
164
+
165
+ # The actual collection data. Must be memoized or you'll access data over
166
+ # and over and over again.
167
+ def collection
168
+ @collection ||= load_collection
169
+ end
170
+
171
+ # Overload this method to add extra find options.
172
+ #
173
+ # :offset and :limit will be overwritten by the pagination_options if the
174
+ # collection is paginated, because you shouldn't be changing the paging
175
+ # directly if you're working with a paginated collection
176
+ #
177
+ def query_options
178
+ {}
179
+ end
180
+
181
+ def load_count
182
+ model_class.count(count_options)
183
+ end
184
+
185
+ # Overload this method to change the way the collection is loaded.
186
+ def load_collection
187
+ model_class.all(find_options)
188
+ end
189
+
190
+ # Raises an AlreadyLoadedError if the collection has already been loaded
191
+ def raise_if_loaded
192
+ raise AlreadyLoadedError, "Cannot modify a collection that has already been loaded." if loaded?
193
+ end
194
+
195
+ # Extracted from AR:B
196
+ # Object#to_a is deprecated, though it does have the desired behavior
197
+ def safe_to_array(o)
198
+ case o
199
+ when NilClass
200
+ []
201
+ when Array
202
+ o
203
+ else
204
+ [o]
205
+ end
206
+ end
207
+ end
208
+
209
+ Base.class_eval do
210
+ include Scope
211
+ include Includes, Order, Pagination
212
+ end
213
+ end
@@ -0,0 +1,56 @@
1
+ module ActiveCollection
2
+ module Includes
3
+
4
+ def self.included(mod)
5
+ mod.extend ClassMethods
6
+ mod.class_eval do
7
+ find_scope :include_options
8
+ end
9
+ end
10
+
11
+ module ClassMethods
12
+ def includes(*includes)
13
+ write_inheritable_attribute(:default_includes, includes)
14
+ end
15
+
16
+ def default_includes
17
+ read_inheritable_attribute(:default_includes) || []
18
+ end
19
+ end
20
+
21
+ def includes
22
+ @includes = self.class.default_includes
23
+ end
24
+
25
+ def include(*includes)
26
+ ac = dup
27
+ ac.include! *includes
28
+ ac
29
+ end
30
+
31
+ def include!(*includes)
32
+ raise_if_loaded
33
+ @includes = (safe_to_array(includes) + safe_to_array(includes)).uniq
34
+ end
35
+
36
+ def include_options
37
+ @includes.blank?? {} : { :include => @includes }
38
+ end
39
+
40
+ protected
41
+
42
+ # Taken from ActiveRecord::Base
43
+ #
44
+ # Object#to_a is deprecated, though it does have the desired behavior
45
+ def safe_to_array(o)
46
+ case o
47
+ when NilClass
48
+ []
49
+ when Array
50
+ o
51
+ else
52
+ [o]
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ module ActiveCollection
2
+ module Order
3
+
4
+ def self.included(mod)
5
+ mod.extend ClassMethods
6
+ mod.class_eval do
7
+ find_scope :order_options
8
+ end
9
+ end
10
+
11
+ def order
12
+ @order ||= self.class.default_order
13
+ end
14
+
15
+ def order_by(order)
16
+ ac = dup
17
+ ac.order_by! order
18
+ ac
19
+ end
20
+
21
+ def order_by!(order)
22
+ raise_if_loaded
23
+ @order = order
24
+ end
25
+
26
+ def order_options
27
+ order ? { :order => order } : {}
28
+ end
29
+
30
+ module ClassMethods
31
+ def order_by(order = "id ASC")
32
+ write_inheritable_attribute(:default_order, order)
33
+ end
34
+
35
+ def default_order
36
+ read_inheritable_attribute(:default_order) || nil
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,189 @@
1
+ module ActiveCollection
2
+ module Pagination
3
+ PER_PAGE = 30
4
+
5
+ def self.included(mod)
6
+ mod.extend ClassMethods
7
+
8
+ mod.class_eval do
9
+ alias_method_chain :total_entries, :pagination
10
+ alias_method_chain :size, :pagination
11
+ find_scope :pagination_options
12
+ end
13
+ end
14
+
15
+ module ClassMethods
16
+ def per_page
17
+ PER_PAGE
18
+ end
19
+ end
20
+
21
+ def current_page
22
+ @current_page ||= params.has_key?(:page) ? (params[:page] || 1).to_i : nil
23
+ end
24
+
25
+ # Defaults to the model class' per_page.
26
+ def per_page
27
+ @per_page ||= params[:per_page] || (model_class.respond_to?(:per_page) && model_class.per_page) || self.class.per_page
28
+ end
29
+ attr_writer :per_page
30
+
31
+ # Loads total entries and calculates the size from that.
32
+ def size_with_pagination
33
+ if paginated?
34
+ last_page?? size_without_pagination % per_page : per_page
35
+ else
36
+ size_without_pagination
37
+ end
38
+ end
39
+
40
+ # Create a new collection for the page specified
41
+ #
42
+ # Optionally accepts a per page parameter which will override the default
43
+ # per_page for the new collection (without changing the current collection).
44
+ def page(pg, per = self.per_page)
45
+ new_collection = self.class.new(params.merge(:page => pg))
46
+ new_collection.per_page = per
47
+ new_collection
48
+ end
49
+
50
+ # Force this collection to a page specified
51
+ #
52
+ # Optionally accepts a per page parameter which will override the per_page
53
+ # for this collection.
54
+ def page!(pg, per = self.per_page)
55
+ raise_if_loaded
56
+ @per_page = per
57
+ @current_page = pg
58
+ end
59
+
60
+ # Helper method that is true when someone tries to fetch a page with a
61
+ # larger number than the last page. Can be used in combination with flashes
62
+ # and redirecting.
63
+ #
64
+ # loads total_entries if not already loaded.
65
+ def out_of_bounds?
66
+ current_page > total_pages
67
+ end
68
+
69
+ # Current offset of the paginated collection. If we're on the first page,
70
+ # it is always 0. If we're on the 2nd page and there are 30 entries per page,
71
+ # the offset is 30. This property is useful if you want to render ordinals
72
+ # side by side with records in the view: simply start with offset + 1.
73
+ #
74
+ # loads total_entries if not already loaded.
75
+ def offset
76
+ (current_page - 1) * per_page
77
+ end
78
+
79
+ # current_page - 1 or nil if there is no previous page.
80
+ def previous_page
81
+ current_page > 1 ? (current_page - 1) : nil
82
+ end
83
+
84
+ # current_page + 1 or nil if there is no next page.
85
+ #
86
+ # loads total_entries if not already loaded.
87
+ def next_page
88
+ current_page < total_pages ? (current_page + 1) : nil
89
+ end
90
+
91
+ # true if the collection is the last page.
92
+ #
93
+ # may load total_entries if not already loaded.
94
+ def last_page?
95
+ next_page.nil?
96
+ end
97
+
98
+ # New Collection for current_page - 1 or nil.
99
+ def previous_page_collection
100
+ previous_page ? page(previous_page, per_page) : nil
101
+ end
102
+
103
+ # New Collection for current_page + 1 or nil
104
+ #
105
+ # loads total_entries if not already loaded.
106
+ def next_page_collection
107
+ next_page ? page(next_page, per_page) : nil
108
+ end
109
+
110
+ # Always returns the total count regardless of pagination.
111
+ #
112
+ # Attempts to save a count query if collection is loaded and is the last page.
113
+ def total_entries_with_pagination
114
+ @total_entries ||=
115
+ if paginated?
116
+ if loaded? and length < per_page and (current_page == 1 or length > 0)
117
+ offset + length
118
+ else
119
+ total_entries_without_pagination
120
+ end
121
+ else
122
+ total_entries_without_pagination
123
+ end
124
+ end
125
+
126
+ # Total number of pages.
127
+ def total_pages
128
+ @total_pages ||= (total_entries / per_page.to_f).ceil
129
+ end
130
+
131
+ # return a paginated collection if it isn't already paginated.
132
+ # returns self if already paginated.
133
+ def paginate
134
+ paginated?? self : page(current_page || 1)
135
+ end
136
+
137
+ # forces pagination of self, raising if already loaded.
138
+ # returns current_page if the collection is now paginated
139
+ # returns nil if already paginated
140
+ def paginate!
141
+ raise_if_loaded
142
+ current_page ? nil : @current_page = 1
143
+ end
144
+
145
+ # if the collection has a page parameter
146
+ def paginated?
147
+ current_page && current_page > 0
148
+ end
149
+
150
+ def as_data_hash
151
+ data_hash = { "collection" => collection.as_json }
152
+ if paginated?
153
+ data_hash["total_entries"] = total_entries
154
+ data_hash["page"] = current_page
155
+ data_hash["per_page"] = per_page
156
+ data_hash["total_pages"] = total_pages
157
+ end
158
+ data_hash
159
+ end
160
+
161
+ def to_xml(options = {})
162
+ collect
163
+ options[:indent] ||= 2
164
+ xml = options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
165
+ xml.instruct! unless options[:skip_instruct]
166
+ xml.tag!(table_name) do
167
+ if paginated?
168
+ xml.total_entries(total_entries, :type => "integer")
169
+ xml.page(current_page, :type => "integer")
170
+ xml.per_page(per_page, :type => "integer")
171
+ xml.total_pages(total_pages, :type => "integer")
172
+ end
173
+ xml.collection(:type => "array") do
174
+ collection.each do |item|
175
+ item.to_xml(:indent => options[:indent], :builder => xml, :skip_instruct => true)
176
+ end
177
+ end
178
+ end
179
+ end
180
+
181
+ protected
182
+
183
+ # Find options for pagination.
184
+ def pagination_options
185
+ paginated?? { :offset => offset, :limit => per_page } : {}
186
+ end
187
+
188
+ end
189
+ end
@@ -0,0 +1,95 @@
1
+ require 'active_support/core_ext/array/extract_options'
2
+
3
+ module ActiveCollection
4
+ module Scope
5
+ #extend ActiveSupport::Concern
6
+
7
+ def self.included(mod)
8
+ mod.extend ClassMethods
9
+ end
10
+
11
+ # Find options for loading the collection.
12
+ #
13
+ # To add more options, define a method that returns a hash with the
14
+ # additional options for find and then add it like this:
15
+ #
16
+ # class BeerCollection
17
+ # find_scope :awesome_beer_only
18
+ #
19
+ # def awesome_beer_only
20
+ # { :conditions => "beer = 'awesome'" }
21
+ # end
22
+ # end
23
+ #
24
+ def find_options
25
+ self.class.scope_for_find.to_options(self)
26
+ end
27
+
28
+ # Count options for loading the total count.
29
+ #
30
+ # To add more options, define a method that returns a hash with the
31
+ # additional options for count and then add it like this:
32
+ #
33
+ # class BeerCollection
34
+ # count_scope :awesome_beer_only
35
+ #
36
+ # def awesome_beer_only
37
+ # { :conditions => "beer = 'awesome'" }
38
+ # end
39
+ # end
40
+ #
41
+ def count_options
42
+ self.class.scope_for_count.to_options(self)
43
+ end
44
+
45
+ module ClassMethods
46
+ def scope_for_find
47
+ ScopeBuilder.new(scope_builder + find_scope_builder)
48
+ end
49
+
50
+ def scope_for_count
51
+ ScopeBuilder.new(scope_builder + count_scope_builder)
52
+ end
53
+
54
+ [:scope, :find_scope, :count_scope].each do |scope|
55
+ module_eval <<-SCOPE, __FILE__, __LINE__
56
+ def #{scope}(*methods, &block)
57
+ #{scope} = ScopeBuilder.build(:#{scope}, *methods, &block)
58
+ @#{scope}_builder ||= ScopeBuilder.new
59
+ @#{scope}_builder.concat #{scope}
60
+ end
61
+
62
+ def #{scope}_builder
63
+ @#{scope}_builder ||= ScopeBuilder.new
64
+ if superclass.respond_to?(:#{scope}_builder)
65
+ superclass.#{scope}_builder + @#{scope}_builder
66
+ else
67
+ @#{scope}_builder
68
+ end
69
+ end
70
+ SCOPE
71
+ end
72
+ end
73
+
74
+ class ScopeBuilder < Array
75
+ def self.build(kind, *methods, &block)
76
+ methods, options = extract_options(*methods, &block)
77
+ methods.map! { |method| ActiveSupport::Callbacks::Callback.new(kind, method, options) }
78
+ new(methods)
79
+ end
80
+
81
+ def to_options(object)
82
+ inject({}) { |h, callback| h.merge( callback.call(object) ) }
83
+ end
84
+
85
+ private
86
+ def self.extract_options(*methods, &block)
87
+ methods.flatten!
88
+ options = methods.extract_options!
89
+ methods << block if block_given?
90
+ return methods, options
91
+ end
92
+ end
93
+ end
94
+ end
95
+
@@ -0,0 +1,109 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class BeerCollection < ActiveCollection::Base
4
+ end
5
+
6
+ class Beer
7
+ end
8
+
9
+ describe ActiveCollection do
10
+ subject { BeerCollection.new }
11
+
12
+ context "(empty)" do
13
+ describe "(count methods)" do
14
+ before do
15
+ Beer.stub!(:count).and_return(0)
16
+ end
17
+
18
+ it "is empty" do
19
+ subject.should be_empty
20
+ end
21
+
22
+ it "has size of 0" do
23
+ subject.size.should == 0
24
+ end
25
+ end
26
+
27
+ describe "(collection loading methods)" do
28
+ before do
29
+ Beer.stub!(:all).and_return([])
30
+ end
31
+
32
+ it "has length of 0" do
33
+ subject.length.should == 0
34
+ end
35
+
36
+ it "doesn't load count after loading the collection" do
37
+ subject.length
38
+ Beer.should_not_receive(:count)
39
+ subject.should be_empty
40
+ end
41
+
42
+ it "doesn't load count after loading the collection" do
43
+ subject.length
44
+ Beer.should_not_receive(:count)
45
+ subject.size.should == 0
46
+ end
47
+
48
+ it "yields no items on each" do
49
+ count = 0
50
+ subject.each { |i| count += 1 }
51
+ count.should == 0
52
+ end
53
+ end
54
+ end
55
+
56
+ context "(simple collection with 5 records)" do
57
+ def records
58
+ @records ||= begin
59
+ beers = []
60
+ 5.times { beers << Beer.new }
61
+ beers
62
+ end
63
+ end
64
+
65
+ describe "(count methods)" do
66
+ before { Beer.stub!(:count).and_return(records.size) }
67
+
68
+ it "is not empty" do
69
+ subject.should_not be_empty
70
+ end
71
+
72
+ it "has a size of 5" do
73
+ subject.size.should == 5
74
+ end
75
+
76
+ it "has total_entries of 5" do
77
+ subject.total_entries.should == 5
78
+ end
79
+ end
80
+
81
+ describe "(collection loading methods)" do
82
+ before do
83
+ Beer.stub!(:all).and_return(records)
84
+ end
85
+
86
+ it "has length of 5" do
87
+ subject.length.should == 5
88
+ end
89
+
90
+ it "doesn't load count after loading the collection" do
91
+ subject.length
92
+ Beer.should_not_receive(:count)
93
+ subject.should_not be_empty
94
+ end
95
+
96
+ it "doesn't load count after loading the collection" do
97
+ subject.length
98
+ Beer.should_not_receive(:count)
99
+ subject.size.should == 5
100
+ end
101
+
102
+ it "yields 5 items to each" do
103
+ count = 0
104
+ subject.each { |i| count += 1 }
105
+ count.should == 5
106
+ end
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,311 @@
1
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
2
+
3
+ class BeerCollection < ActiveCollection::Base
4
+ end
5
+
6
+ class Beer
7
+ end
8
+
9
+ describe "an empty collection", :shared => true do
10
+ it "is empty" do
11
+ subject.should be_empty
12
+ end
13
+
14
+ it "has 0 total entries" do
15
+ subject.total_entries.should == 0
16
+ end
17
+
18
+ it "has 0 total_pages" do
19
+ subject.total_pages.should == 0
20
+ end
21
+
22
+ it "has length of 0" do
23
+ subject.length.should == 0
24
+ end
25
+
26
+ it "yields no items on each" do
27
+ count = 0
28
+ subject.each { |i| count += 1 }
29
+ count.should == 0
30
+ end
31
+ end
32
+
33
+ describe ActiveCollection do
34
+ subject { BeerCollection.new(:page => 1) }
35
+
36
+ context "(empty)" do
37
+ before do
38
+ Beer.stub!(:count).and_return(0)
39
+ Beer.stub!(:all).and_return([])
40
+ end
41
+
42
+ describe "(page 1)" do
43
+ it_should_behave_like "an empty collection"
44
+
45
+ it "is on page 1" do
46
+ subject.current_page.should == 1
47
+ end
48
+ end
49
+
50
+ describe "(page 2)" do
51
+ subject { BeerCollection.new(:page => 2) }
52
+
53
+ it_should_behave_like "an empty collection"
54
+
55
+ it "should be out of bounds" do
56
+ subject.should be_out_of_bounds
57
+ end
58
+
59
+ it "is on page 2" do
60
+ subject.current_page.should == 2
61
+ end
62
+ end
63
+ end
64
+
65
+ context "(simple collection with 5 records)" do
66
+ def records
67
+ @records ||= begin
68
+ beers = []
69
+ 5.times { beers << Beer.new }
70
+ beers
71
+ end
72
+ end
73
+ before { Beer.stub!(:count).and_return(records.size) }
74
+
75
+ describe "(default per_page)" do
76
+ before do
77
+ Beer.stub!(:all).with(
78
+ :limit => ActiveCollection::Base.per_page,
79
+ :offset => 0
80
+ ).and_return(records)
81
+ end
82
+
83
+ describe "(page 1)" do
84
+ it "is not empty" do
85
+ subject.should_not be_empty
86
+ end
87
+
88
+ it "has a size of 5" do
89
+ subject.size.should == 5
90
+ end
91
+
92
+ it "has total_entries of 5" do
93
+ subject.total_entries.should == 5
94
+ end
95
+
96
+ it "has length of 5" do
97
+ subject.length.should == 5
98
+ end
99
+
100
+ it "doesn't load count after loading the collection" do
101
+ subject.length
102
+ Beer.should_not_receive(:count)
103
+ subject.empty?
104
+ subject.size
105
+ subject.total_entries
106
+ end
107
+
108
+ it "yields 5 items to each" do
109
+ count = 0
110
+ subject.each { |i| count += 1 }
111
+ count.should == 5
112
+ end
113
+
114
+ it "has 1 total pages" do
115
+ subject.total_pages.should == 1
116
+ end
117
+
118
+ it "is on page 1" do
119
+ subject.current_page.should == 1
120
+ end
121
+
122
+ it "is the last page" do
123
+ subject.should be_last_page
124
+ end
125
+
126
+ it "has no next page" do
127
+ subject.next_page.should be_nil
128
+ end
129
+
130
+ it "has no previous page" do
131
+ subject.previous_page.should be_nil
132
+ end
133
+
134
+ it "returs nil next page collection" do
135
+ subject.next_page_collection.should be_nil
136
+ end
137
+
138
+ it "returs nil previous page collection" do
139
+ subject.previous_page_collection.should be_nil
140
+ end
141
+
142
+ it "has default per_page" do
143
+ subject.per_page.should == ActiveCollection::Base.per_page
144
+ end
145
+
146
+ it "is not out of bounds" do
147
+ subject.should_not be_out_of_bounds
148
+ end
149
+
150
+ it "has a 0 offset" do
151
+ subject.offset.should == 0
152
+ end
153
+
154
+ it "loads records using default limit and 0 offset" do
155
+ Beer.should_receive(:all).with(:limit => ActiveCollection::Base.per_page, :offset => 0).and_return(records)
156
+ subject.length
157
+ end
158
+ end
159
+ end
160
+
161
+ context "(2 per page)" do
162
+ describe "(page 1)" do
163
+ before { Beer.stub!(:all).with(:limit => 2, :offset => 0).and_return(records[0..1]) }
164
+ subject { BeerCollection.new(:page => 1, :per_page => 2) }
165
+
166
+ it "is not empty" do
167
+ subject.should_not be_empty
168
+ end
169
+
170
+ it "has a size of 2" do
171
+ subject.size.should == 2
172
+ end
173
+
174
+ it "has a length of 2" do
175
+ subject.length.should == 2
176
+ end
177
+
178
+ it "has 5 total entries" do
179
+ subject.total_entries.should == 5
180
+ end
181
+
182
+ it "has 3 total pages" do
183
+ subject.total_pages.should == 3
184
+ end
185
+
186
+ it "is on page 1" do
187
+ subject.current_page.should == 1
188
+ end
189
+
190
+ it "is not the last page" do
191
+ subject.should_not be_last_page
192
+ end
193
+
194
+ it "has next page of 2" do
195
+ subject.next_page.should == 2
196
+ end
197
+
198
+ it "has no previous page" do
199
+ subject.previous_page.should be_nil
200
+ end
201
+
202
+ it "returs a next page collection for page 2" do
203
+ nex = subject.next_page_collection
204
+ nex.should_not be_nil
205
+ nex.current_page.should == 2
206
+ end
207
+
208
+ it "returs nil previous page collection" do
209
+ subject.previous_page_collection.should be_nil
210
+ end
211
+
212
+ it "has per_page of 2" do
213
+ subject.per_page.should == 2
214
+ end
215
+
216
+ it "is not out of bounds" do
217
+ subject.should_not be_out_of_bounds
218
+ end
219
+
220
+ it "has a 0 offset" do
221
+ subject.offset.should == 0
222
+ end
223
+
224
+ it "calls count to find total_entries even when collection is loaded" do
225
+ subject.length
226
+ Beer.should_receive(:count).and_return(5)
227
+ subject.total_entries.should == 5
228
+ end
229
+
230
+ it "loads records using default limit and 0 offset" do
231
+ Beer.should_receive(:all).with(:limit => 2, :offset => 0).and_return(records)
232
+ subject.length
233
+ end
234
+ end
235
+
236
+ describe "(page 3)" do
237
+ before { Beer.stub!(:all).with(:limit => 2, :offset => 4).and_return(records[4..4]) }
238
+ subject { BeerCollection.new(:page => 3, :per_page => 2) }
239
+
240
+ it "is not empty" do
241
+ subject.should_not be_empty
242
+ end
243
+
244
+ it "has a size of 1" do
245
+ subject.size.should == 1
246
+ end
247
+
248
+ it "has a length of 1" do
249
+ subject.length.should == 1
250
+ end
251
+
252
+ it "has 5 total entries" do
253
+ subject.total_entries.should == 5
254
+ end
255
+
256
+ it "has 3 total pages" do
257
+ subject.total_pages.should == 3
258
+ end
259
+
260
+ it "is on page 3" do
261
+ subject.current_page.should == 3
262
+ end
263
+
264
+ it "is the last page" do
265
+ subject.should be_last_page
266
+ end
267
+
268
+ it "has no next page" do
269
+ subject.next_page.should be_nil
270
+ end
271
+
272
+ it "has previous page of 2" do
273
+ subject.previous_page.should == 2
274
+ end
275
+
276
+ it "returs nil next page collection" do
277
+ subject.next_page_collection.should be_nil
278
+ end
279
+
280
+ it "returs nil previous page collection" do
281
+ prev = subject.previous_page_collection
282
+ prev.should_not be_nil
283
+ prev.current_page.should == 2
284
+ end
285
+
286
+ it "has per_page of 2" do
287
+ subject.per_page.should == 2
288
+ end
289
+
290
+ it "is not out of bounds" do
291
+ subject.should_not be_out_of_bounds
292
+ end
293
+
294
+ it "has a 0 offset" do
295
+ subject.offset.should == 4
296
+ end
297
+
298
+ it "does not call count to find total_entries when collection is loaded" do
299
+ subject.length
300
+ Beer.should_not_receive(:count).and_return(5)
301
+ subject.total_entries.should == 5
302
+ end
303
+
304
+ it "loads records using default limit and 0 offset" do
305
+ Beer.should_receive(:all).with(:limit => 2, :offset => 4).and_return(records)
306
+ subject.length
307
+ end
308
+ end
309
+ end
310
+ end
311
+ end
data/spec/spec.opts ADDED
@@ -0,0 +1,4 @@
1
+ --colour
2
+ --format progress
3
+ --loadby mtime
4
+ --reverse
@@ -0,0 +1,10 @@
1
+ $LOAD_PATH.unshift(File.dirname(__FILE__))
2
+ $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
3
+ require 'rubygems'
4
+ require 'active_collection'
5
+ require 'spec'
6
+ require 'spec/autorun'
7
+
8
+ Spec::Runner.configure do |config|
9
+
10
+ end
metadata ADDED
@@ -0,0 +1,83 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: active_collection
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Martin Emde
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-09-14 00:00:00 -07:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rspec
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ description: Lazy-loaded array of records
26
+ email: martin.emde@gmail.com
27
+ executables: []
28
+
29
+ extensions: []
30
+
31
+ extra_rdoc_files:
32
+ - LICENSE
33
+ - README.rdoc
34
+ files:
35
+ - .document
36
+ - .gitignore
37
+ - LICENSE
38
+ - README.rdoc
39
+ - Rakefile
40
+ - VERSION.yml
41
+ - active_collection.gemspec
42
+ - lib/active_collection.rb
43
+ - lib/active_collection/base.rb
44
+ - lib/active_collection/includes.rb
45
+ - lib/active_collection/order.rb
46
+ - lib/active_collection/pagination.rb
47
+ - lib/active_collection/scope.rb
48
+ - spec/active_collection_spec.rb
49
+ - spec/pagination_spec.rb
50
+ - spec/spec.opts
51
+ - spec/spec_helper.rb
52
+ has_rdoc: true
53
+ homepage: http://github.com/martinemde/active_collection
54
+ licenses: []
55
+
56
+ post_install_message:
57
+ rdoc_options:
58
+ - --charset=UTF-8
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: "0"
66
+ version:
67
+ required_rubygems_version: !ruby/object:Gem::Requirement
68
+ requirements:
69
+ - - ">="
70
+ - !ruby/object:Gem::Version
71
+ version: "0"
72
+ version:
73
+ requirements: []
74
+
75
+ rubyforge_project: collection
76
+ rubygems_version: 1.3.5
77
+ signing_key:
78
+ specification_version: 3
79
+ summary: Lazy-loaded array of records
80
+ test_files:
81
+ - spec/active_collection_spec.rb
82
+ - spec/pagination_spec.rb
83
+ - spec/spec_helper.rb