couchrest_model 1.1.2 → 1.2.0.beta

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/README.md +8 -2
  2. data/VERSION +1 -1
  3. data/couchrest_model.gemspec +2 -1
  4. data/history.md +8 -0
  5. data/lib/couchrest/model/base.rb +0 -20
  6. data/lib/couchrest/model/configuration.rb +2 -0
  7. data/lib/couchrest/model/core_extensions/time_parsing.rb +35 -9
  8. data/lib/couchrest/model/designs/design.rb +182 -0
  9. data/lib/couchrest/model/designs/view.rb +91 -48
  10. data/lib/couchrest/model/designs.rb +72 -19
  11. data/lib/couchrest/model/document_queries.rb +15 -45
  12. data/lib/couchrest/model/properties.rb +43 -2
  13. data/lib/couchrest/model/proxyable.rb +20 -54
  14. data/lib/couchrest/model/typecast.rb +1 -1
  15. data/lib/couchrest/model/validations/uniqueness.rb +7 -6
  16. data/lib/couchrest_model.rb +1 -5
  17. data/spec/fixtures/models/article.rb +22 -20
  18. data/spec/fixtures/models/base.rb +15 -7
  19. data/spec/fixtures/models/course.rb +7 -4
  20. data/spec/fixtures/models/project.rb +4 -1
  21. data/spec/fixtures/models/sale_entry.rb +5 -3
  22. data/spec/unit/base_spec.rb +51 -5
  23. data/spec/unit/core_extensions/time_parsing.rb +41 -0
  24. data/spec/unit/designs/design_spec.rb +291 -0
  25. data/spec/unit/designs/view_spec.rb +135 -40
  26. data/spec/unit/designs_spec.rb +341 -30
  27. data/spec/unit/dirty_spec.rb +67 -0
  28. data/spec/unit/inherited_spec.rb +2 -2
  29. data/spec/unit/property_protection_spec.rb +3 -1
  30. data/spec/unit/property_spec.rb +43 -3
  31. data/spec/unit/proxyable_spec.rb +57 -98
  32. data/spec/unit/subclass_spec.rb +14 -5
  33. data/spec/unit/validations_spec.rb +14 -12
  34. metadata +172 -129
  35. data/lib/couchrest/model/class_proxy.rb +0 -135
  36. data/lib/couchrest/model/collection.rb +0 -273
  37. data/lib/couchrest/model/design_doc.rb +0 -115
  38. data/lib/couchrest/model/support/couchrest_design.rb +0 -33
  39. data/lib/couchrest/model/views.rb +0 -148
  40. data/spec/unit/class_proxy_spec.rb +0 -167
  41. data/spec/unit/collection_spec.rb +0 -86
  42. data/spec/unit/design_doc_spec.rb +0 -212
  43. data/spec/unit/view_spec.rb +0 -352
@@ -1,273 +0,0 @@
1
- module CouchRest
2
- module Model
3
- # Warning! The Collection module is seriously depricated.
4
- # Use the new Design Views instead, as this code copies many other parts
5
- # of CouchRest Model.
6
- #
7
- # Expect this to be removed soon.
8
- #
9
- module Collection
10
-
11
- def self.included(base)
12
- base.extend(ClassMethods)
13
- end
14
-
15
- module ClassMethods
16
-
17
- # Creates a new class method, find_all_<collection_name>, that will
18
- # execute the view specified with the design_doc and view_name
19
- # parameters, along with the specified view_options. This method will
20
- # return the results of the view as an Array of objects which are
21
- # instances of the class.
22
- #
23
- # This method is handy for objects that do not use the view_by method
24
- # to declare their views.
25
- def provides_collection(collection_name, design_doc, view_name, view_options)
26
- class_eval <<-END, __FILE__, __LINE__ + 1
27
- def self.find_all_#{collection_name}(options = {})
28
- view_options = #{view_options.inspect} || {}
29
- CollectionProxy.new(options[:database] || database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
30
- end
31
- END
32
- end
33
-
34
- # Fetch a group of objects from CouchDB. Options can include:
35
- # :page - Specifies the page to load (starting at 1)
36
- # :per_page - Specifies the number of objects to load per page
37
- #
38
- # Defaults are used if these options are not specified.
39
- def paginate(options)
40
- proxy = create_collection_proxy(options)
41
- proxy.paginate(options)
42
- end
43
-
44
- # Iterate over the objects in a collection, fetching them from CouchDB
45
- # in groups. Options can include:
46
- # :page - Specifies the page to load
47
- # :per_page - Specifies the number of objects to load per page
48
- #
49
- # Defaults are used if these options are not specified.
50
- def paginated_each(options, &block)
51
- search = options.delete(:search)
52
- unless search == true
53
- proxy = create_collection_proxy(options)
54
- else
55
- proxy = create_search_collection_proxy(options)
56
- end
57
- proxy.paginated_each(options, &block)
58
- end
59
-
60
- # Create a CollectionProxy for the specified view and options.
61
- # CollectionProxy behaves just like an Array, but offers support for
62
- # pagination.
63
- def collection_proxy_for(design_doc, view_name, view_options = {})
64
- options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
65
- create_collection_proxy(options)
66
- end
67
-
68
- private
69
-
70
- def create_collection_proxy(options)
71
- design_doc, view_name, view_options = parse_view_options(options)
72
- CollectionProxy.new(options[:database] || database, design_doc, view_name, view_options, self)
73
- end
74
-
75
- def create_search_collection_proxy(options)
76
- design_doc, search_name, search_options = parse_search_options(options)
77
- CollectionProxy.new(options[:database] || database, design_doc, search_name, search_options, self, :search)
78
- end
79
-
80
- def parse_view_options(options)
81
- design_doc = options.delete(:design_doc)
82
- raise ArgumentError, 'design_doc is required' if design_doc.nil?
83
-
84
- view_name = options.delete(:view_name)
85
- raise ArgumentError, 'view_name is required' if view_name.nil?
86
-
87
- default_view_options = (design_doc.class == Design &&
88
- design_doc['views'][view_name.to_s] &&
89
- design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
90
- view_options = default_view_options.merge(options)
91
- view_options.delete(:database)
92
-
93
- [design_doc, view_name, view_options]
94
- end
95
-
96
- def parse_search_options(options)
97
- design_doc = options.delete(:design_doc)
98
- raise ArgumentError, 'design_doc is required' if design_doc.nil?
99
-
100
- search_name = options.delete(:view_name)
101
- raise ArgumentError, 'search_name is required' if search_name.nil?
102
-
103
- search_options = options.clone
104
- search_options.delete(:database)
105
-
106
- [design_doc, search_name, search_options]
107
- end
108
-
109
- end
110
-
111
- class CollectionProxy
112
- alias_method :proxy_respond_to?, :respond_to?
113
- instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
114
-
115
- DEFAULT_PAGE = 1
116
- DEFAULT_PER_PAGE = 30
117
-
118
- # Create a new CollectionProxy to represent the specified view. If a
119
- # container class is specified, the proxy will create an object of the
120
- # given type for each row that comes back from the view. If no
121
- # container class is specified, the raw results are returned.
122
- #
123
- # The CollectionProxy provides support for paginating over a collection
124
- # via the paginate, and paginated_each methods.
125
- def initialize(database, design_doc, view_name, view_options = {}, container_class = nil, query_type = :view)
126
- raise ArgumentError, "database is a required parameter" if database.nil?
127
-
128
- @database = database
129
- @container_class = container_class
130
- @query_type = query_type
131
-
132
- strip_pagination_options(view_options)
133
- @view_options = view_options
134
-
135
- if design_doc.class == Design
136
- @view_name = "#{design_doc.name}/#{view_name}"
137
- else
138
- @view_name = "#{design_doc}/#{view_name}"
139
- end
140
-
141
- # Save the design doc, ready for use
142
- @container_class.save_design_doc(@database)
143
- end
144
-
145
- # See Collection.paginate
146
- def paginate(options = {})
147
- page, per_page = parse_options(options)
148
- results = @database.send(@query_type, @view_name, pagination_options(page, per_page))
149
- remember_where_we_left_off(results, page)
150
- instances = convert_to_container_array(results)
151
-
152
- begin
153
- if Kernel.const_get('WillPaginate')
154
- total_rows = results['total_rows'].to_i
155
- paginated = WillPaginate::Collection.create(page, per_page, total_rows) do |pager|
156
- pager.replace(instances)
157
- end
158
- return paginated
159
- end
160
- rescue NameError
161
- # When not using will_paginate, not much we could do about this. :x
162
- end
163
- return instances
164
- end
165
-
166
- # See Collection.paginated_each
167
- def paginated_each(options = {}, &block)
168
- page, per_page = parse_options(options)
169
-
170
- begin
171
- collection = paginate({:page => page, :per_page => per_page})
172
- collection.each(&block)
173
- page += 1
174
- end until collection.size < per_page
175
- end
176
-
177
- def respond_to?(*args)
178
- proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
179
- end
180
-
181
- # Explicitly proxy === because the instance method removal above
182
- # doesn't catch it.
183
- def ===(other)
184
- load_target
185
- other === @target
186
- end
187
-
188
- private
189
-
190
- def method_missing(method, *args)
191
- if load_target
192
- if block_given?
193
- @target.send(method, *args) { |*block_args| yield(*block_args) }
194
- else
195
- @target.send(method, *args)
196
- end
197
- end
198
- end
199
-
200
- def load_target
201
- unless loaded?
202
- @view_options.merge!({:include_docs => true}) if @query_type == :search
203
- results = @database.send(@query_type, @view_name, @view_options)
204
- @target = convert_to_container_array(results)
205
- end
206
- @loaded = true
207
- @target
208
- end
209
-
210
- def loaded?
211
- @loaded
212
- end
213
-
214
- def reload
215
- reset
216
- load_target
217
- self unless @target.nil?
218
- end
219
-
220
- def reset
221
- @loaded = false
222
- @target = nil
223
- end
224
-
225
- def inspect
226
- load_target
227
- @target.inspect
228
- end
229
-
230
- def convert_to_container_array(results)
231
- if @container_class.nil?
232
- results
233
- else
234
- results['rows'].collect { |row| @container_class.build_from_database(row['doc']) } unless results['rows'].nil?
235
- end
236
- end
237
-
238
- def pagination_options(page, per_page)
239
- view_options = @view_options.clone
240
- if @query_type == :view && @last_key && @last_docid && @last_page == page - 1
241
- key = view_options.delete(:key)
242
- end_key = view_options[:endkey] || key
243
- options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
244
- else
245
- options = { :limit => per_page, :skip => per_page * (page - 1) }
246
- end
247
- options[:include_docs] = true
248
- view_options.merge(options)
249
- end
250
-
251
- def parse_options(options)
252
- page = options.delete(:page) || DEFAULT_PAGE
253
- per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
254
- [page.to_i, per_page.to_i]
255
- end
256
-
257
- def strip_pagination_options(options)
258
- parse_options(options)
259
- end
260
-
261
- def remember_where_we_left_off(results, page)
262
- last_row = results['rows'].last
263
- if last_row
264
- @last_key = last_row['key']
265
- @last_docid = last_row['id']
266
- end
267
- @last_page = page
268
- end
269
- end
270
-
271
- end
272
- end
273
- end
@@ -1,115 +0,0 @@
1
- # encoding: utf-8
2
- module CouchRest
3
- module Model
4
- module DesignDoc
5
- extend ActiveSupport::Concern
6
-
7
- module ClassMethods
8
-
9
- def design_doc
10
- @design_doc ||= ::CouchRest::Design.new(default_design_doc)
11
- end
12
-
13
- def design_doc_id
14
- "_design/#{design_doc_slug}"
15
- end
16
-
17
- def design_doc_slug
18
- self.to_s
19
- end
20
-
21
- def design_doc_uri(db = database)
22
- "#{db.root}/#{design_doc_id}"
23
- end
24
-
25
- # Retreive the latest version of the design document directly
26
- # from the database. This is never cached and will return nil if
27
- # the design is not present.
28
- #
29
- # Use this method if you'd like to compare revisions [_rev] which
30
- # is not stored in the normal design doc.
31
- def stored_design_doc(db = database)
32
- db.get(design_doc_id)
33
- rescue RestClient::ResourceNotFound
34
- nil
35
- end
36
-
37
- # Save the design doc onto a target database in a thread-safe way,
38
- # not modifying the model's design_doc
39
- #
40
- # See also save_design_doc! to always save the design doc even if there
41
- # are no changes.
42
- def save_design_doc(db = database, force = false)
43
- update_design_doc(db, force)
44
- end
45
-
46
- # Force the update of the model's design_doc even if it hasn't changed.
47
- def save_design_doc!(db = database)
48
- save_design_doc(db, true)
49
- end
50
-
51
- private
52
-
53
- def design_doc_cache
54
- Thread.current[:couchrest_design_cache] ||= {}
55
- end
56
- def design_doc_cache_checksum(db)
57
- design_doc_cache[design_doc_uri(db)]
58
- end
59
- def set_design_doc_cache_checksum(db, checksum)
60
- design_doc_cache[design_doc_uri(db)] = checksum
61
- end
62
-
63
- # Writes out a design_doc to a given database if forced
64
- # or the stored checksum is not the same as the current
65
- # generated checksum.
66
- #
67
- # Returns the original design_doc provided, but does
68
- # not update it with the revision.
69
- def update_design_doc(db, force = false)
70
- return design_doc unless force || auto_update_design_doc
71
-
72
- # Grab the design doc's checksum
73
- checksum = design_doc.checksum!
74
-
75
- # If auto updates enabled, check checksum cache
76
- return design_doc if auto_update_design_doc && design_doc_cache_checksum(db) == checksum
77
-
78
- # Load up the stored doc (if present), update, and save
79
- saved = stored_design_doc(db)
80
- if saved
81
- if force || saved['couchrest-hash'] != checksum
82
- saved.merge!(design_doc)
83
- db.save_doc(saved)
84
- end
85
- else
86
- db.save_doc(design_doc)
87
- design_doc.delete('_rev') # Prevent conflicts, never store rev as DB specific
88
- end
89
-
90
- # Ensure checksum cached for next attempt if using auto updates
91
- set_design_doc_cache_checksum(db, checksum) if auto_update_design_doc
92
- design_doc
93
- end
94
-
95
- def default_design_doc
96
- {
97
- "_id" => design_doc_id,
98
- "language" => "javascript",
99
- "views" => {
100
- 'all' => {
101
- 'map' => "function(doc) {
102
- if (doc['#{self.model_type_key}'] == '#{self.to_s}') {
103
- emit(doc['_id'],1);
104
- }
105
- }"
106
- }
107
- }
108
- }
109
- end
110
-
111
- end # module ClassMethods
112
-
113
- end
114
- end
115
- end
@@ -1,33 +0,0 @@
1
-
2
- CouchRest::Design.class_eval do
3
-
4
- # Calculate and update the checksum of the Design document.
5
- # Used for ensuring the latest version has been sent to the database.
6
- #
7
- # This will generate an flatterned, ordered array of all the elements of the
8
- # design document, convert to string then generate an MD5 Hash. This should
9
- # result in a consisitent Hash accross all platforms.
10
- #
11
- def checksum!
12
- # create a copy of basic elements
13
- base = self.dup
14
- base.delete('_id')
15
- base.delete('_rev')
16
- base.delete('couchrest-hash')
17
- result = nil
18
- flatten =
19
- lambda {|r|
20
- (recurse = lambda {|v|
21
- if v.is_a?(Hash) || v.is_a?(CouchRest::Document)
22
- v.to_a.map{|v| recurse.call(v)}.flatten
23
- elsif v.is_a?(Array)
24
- v.flatten.map{|v| recurse.call(v)}
25
- else
26
- v.to_s
27
- end
28
- }).call(r)
29
- }
30
- self['couchrest-hash'] = Digest::MD5.hexdigest(flatten.call(base).sort.join(''))
31
- end
32
-
33
- end
@@ -1,148 +0,0 @@
1
- module CouchRest
2
- module Model
3
- module Views
4
- extend ActiveSupport::Concern
5
-
6
- module ClassMethods
7
- # Define a CouchDB view. The name of the view will be the concatenation
8
- # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
9
- #
10
- # ==== Example views:
11
- #
12
- # class Post
13
- # # view with default options
14
- # # query with Post.by_date
15
- # view_by :date, :descending => true
16
- #
17
- # # view with compound sort-keys
18
- # # query with Post.by_user_id_and_date
19
- # view_by :user_id, :date
20
- #
21
- # # view with custom map/reduce functions
22
- # # query with Post.by_tags :reduce => true
23
- # view_by :tags,
24
- # :map =>
25
- # "function(doc) {
26
- # if (doc['model'] == 'Post' && doc.tags) {
27
- # doc.tags.forEach(function(tag){
28
- # emit(doc.tag, 1);
29
- # });
30
- # }
31
- # }",
32
- # :reduce =>
33
- # "function(keys, values, rereduce) {
34
- # return sum(values);
35
- # }"
36
- # end
37
- #
38
- # <tt>view_by :date</tt> will create a view defined by this Javascript
39
- # function:
40
- #
41
- # function(doc) {
42
- # if (doc['model'] == 'Post' && doc.date) {
43
- # emit(doc.date, null);
44
- # }
45
- # }
46
- #
47
- # It can be queried by calling <tt>Post.by_date</tt> which accepts all
48
- # valid options for CouchRest::Database#view. In addition, calling with
49
- # the <tt>:raw => true</tt> option will return the view rows
50
- # themselves. By default <tt>Post.by_date</tt> will return the
51
- # documents included in the generated view.
52
- #
53
- # Calling with :database => [instance of CouchRest::Database] will
54
- # send the query to a specific database, otherwise it will go to
55
- # the model's default database (use_database)
56
- #
57
- # CouchRest::Database#view options can be applied at view definition
58
- # time as defaults, and they will be curried and used at view query
59
- # time. Or they can be overridden at query time.
60
- #
61
- # Custom views can be queried with <tt>:reduce => true</tt> to return
62
- # reduce results. The default for custom views is to query with
63
- # <tt>:reduce => false</tt>.
64
- #
65
- # Views are generated (on a per-model basis) lazily on first-access.
66
- # This means that if you are deploying changes to a view, the views for
67
- # that model won't be available until generation is complete. This can
68
- # take some time with large databases. Strategies are in the works.
69
- #
70
- # To understand the capabilities of this view system more completely,
71
- # it is recommended that you read the RSpec file at
72
- # <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
73
-
74
- def view_by(*keys)
75
- opts = keys.pop if keys.last.is_a?(Hash)
76
- opts ||= {}
77
- ducktype = opts.delete(:ducktype)
78
- unless ducktype || opts[:map]
79
- opts[:guards] ||= []
80
- opts[:guards].push "(doc['#{model_type_key}'] == '#{self.to_s}')"
81
- end
82
- keys.push opts
83
- design_doc.view_by(*keys)
84
- end
85
-
86
- # returns stored defaults if there is a view named this in the design doc
87
- def has_view?(name)
88
- design_doc && design_doc.has_view?(name)
89
- end
90
-
91
- # Check if the view can be reduced by checking to see if it has a
92
- # reduce function.
93
- def can_reduce_view?(name)
94
- design_doc && design_doc.can_reduce_view?(name)
95
- end
96
-
97
- # Dispatches to any named view.
98
- def view(name, query={}, &block)
99
- query = query.dup # Modifications made on copy!
100
- db = query.delete(:database) || database
101
- query[:raw] = true if query[:reduce]
102
- raw = query.delete(:raw)
103
- save_design_doc(db)
104
- fetch_view_with_docs(db, name, query, raw, &block)
105
- end
106
-
107
- # Find the first entry in the view. If the second parameter is a string
108
- # it will be used as the key for the request, for example:
109
- #
110
- # Course.first_from_view('by_teacher', 'Fred')
111
- #
112
- # More advanced requests can be performed by providing a hash:
113
- #
114
- # Course.first_from_view('by_teacher', :startkey => 'bbb', :endkey => 'eee')
115
- #
116
- def first_from_view(name, *args)
117
- query = {:limit => 1}
118
- case args.first
119
- when String, Array
120
- query.update(args[1]) unless args[1].nil?
121
- query[:key] = args.first
122
- when Hash
123
- query.update(args.first)
124
- end
125
- view(name, query).first
126
- end
127
-
128
- private
129
-
130
- def fetch_view_with_docs(db, name, opts, raw=false, &block)
131
- if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
132
- fetch_view(db, name, opts, &block)
133
- else
134
- opts = opts.merge(:include_docs => true)
135
- view = fetch_view db, name, opts, &block
136
- view['rows'].collect{|r| build_from_database(r['doc'])} if view['rows']
137
- end
138
- end
139
-
140
- def fetch_view(db, view_name, opts, &block)
141
- raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
142
- design_doc.view_on(db, view_name, opts, &block)
143
- end
144
- end # module ClassMethods
145
-
146
- end
147
- end
148
- end