active_collection 0.2.0

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