couchrest_model 1.1.2 → 1.2.0.beta

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.
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