couchrest_model-radiant 1.0.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.
Files changed (73) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +19 -0
  3. data/Rakefile +74 -0
  4. data/THANKS.md +21 -0
  5. data/history.txt +207 -0
  6. data/lib/couchrest/model.rb +10 -0
  7. data/lib/couchrest/model/associations.rb +223 -0
  8. data/lib/couchrest/model/base.rb +111 -0
  9. data/lib/couchrest/model/callbacks.rb +27 -0
  10. data/lib/couchrest/model/casted_array.rb +39 -0
  11. data/lib/couchrest/model/casted_model.rb +68 -0
  12. data/lib/couchrest/model/class_proxy.rb +122 -0
  13. data/lib/couchrest/model/collection.rb +263 -0
  14. data/lib/couchrest/model/configuration.rb +51 -0
  15. data/lib/couchrest/model/design_doc.rb +123 -0
  16. data/lib/couchrest/model/document_queries.rb +83 -0
  17. data/lib/couchrest/model/errors.rb +23 -0
  18. data/lib/couchrest/model/extended_attachments.rb +77 -0
  19. data/lib/couchrest/model/persistence.rb +155 -0
  20. data/lib/couchrest/model/properties.rb +208 -0
  21. data/lib/couchrest/model/property.rb +97 -0
  22. data/lib/couchrest/model/property_protection.rb +71 -0
  23. data/lib/couchrest/model/support/couchrest.rb +19 -0
  24. data/lib/couchrest/model/support/hash.rb +9 -0
  25. data/lib/couchrest/model/typecast.rb +175 -0
  26. data/lib/couchrest/model/validations.rb +68 -0
  27. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  28. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  29. data/lib/couchrest/model/validations/uniqueness.rb +44 -0
  30. data/lib/couchrest/model/views.rb +160 -0
  31. data/lib/couchrest/railtie.rb +12 -0
  32. data/lib/couchrest_model.rb +62 -0
  33. data/lib/rails/generators/couchrest_model.rb +16 -0
  34. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  35. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  36. data/spec/couchrest/assocations_spec.rb +196 -0
  37. data/spec/couchrest/attachment_spec.rb +176 -0
  38. data/spec/couchrest/base_spec.rb +463 -0
  39. data/spec/couchrest/casted_model_spec.rb +438 -0
  40. data/spec/couchrest/casted_spec.rb +75 -0
  41. data/spec/couchrest/class_proxy_spec.rb +132 -0
  42. data/spec/couchrest/configuration_spec.rb +78 -0
  43. data/spec/couchrest/inherited_spec.rb +40 -0
  44. data/spec/couchrest/persistence_spec.rb +415 -0
  45. data/spec/couchrest/property_protection_spec.rb +192 -0
  46. data/spec/couchrest/property_spec.rb +871 -0
  47. data/spec/couchrest/subclass_spec.rb +99 -0
  48. data/spec/couchrest/validations.rb +85 -0
  49. data/spec/couchrest/view_spec.rb +463 -0
  50. data/spec/fixtures/attachments/README +3 -0
  51. data/spec/fixtures/attachments/couchdb.png +0 -0
  52. data/spec/fixtures/attachments/test.html +11 -0
  53. data/spec/fixtures/base.rb +139 -0
  54. data/spec/fixtures/more/article.rb +35 -0
  55. data/spec/fixtures/more/card.rb +17 -0
  56. data/spec/fixtures/more/cat.rb +19 -0
  57. data/spec/fixtures/more/client.rb +6 -0
  58. data/spec/fixtures/more/course.rb +25 -0
  59. data/spec/fixtures/more/event.rb +8 -0
  60. data/spec/fixtures/more/invoice.rb +14 -0
  61. data/spec/fixtures/more/person.rb +9 -0
  62. data/spec/fixtures/more/question.rb +7 -0
  63. data/spec/fixtures/more/sale_entry.rb +9 -0
  64. data/spec/fixtures/more/sale_invoice.rb +13 -0
  65. data/spec/fixtures/more/service.rb +10 -0
  66. data/spec/fixtures/more/user.rb +22 -0
  67. data/spec/fixtures/views/lib.js +3 -0
  68. data/spec/fixtures/views/test_view/lib.js +3 -0
  69. data/spec/fixtures/views/test_view/only-map.js +4 -0
  70. data/spec/fixtures/views/test_view/test-map.js +3 -0
  71. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  72. data/spec/spec_helper.rb +48 -0
  73. metadata +263 -0
@@ -0,0 +1,122 @@
1
+ module CouchRest
2
+ module Model
3
+ module ClassProxy
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Return a proxy object which represents a model class on a
12
+ # chosen database instance. This allows you to DRY operations
13
+ # where a database is chosen dynamically.
14
+ #
15
+ # ==== Example:
16
+ #
17
+ # db = CouchRest::Database.new(...)
18
+ # articles = Article.on(db)
19
+ #
20
+ # articles.all { ... }
21
+ # articles.by_title { ... }
22
+ #
23
+ # u = articles.get("someid")
24
+ #
25
+ # u = articles.new(:title => "I like plankton")
26
+ # u.save # saved on the correct database
27
+
28
+ def on(database)
29
+ Proxy.new(self, database)
30
+ end
31
+ end
32
+
33
+ class Proxy #:nodoc:
34
+ def initialize(klass, database)
35
+ @klass = klass
36
+ @database = database
37
+ end
38
+
39
+ # Base
40
+
41
+ def new(*args)
42
+ doc = @klass.new(*args)
43
+ doc.database = @database
44
+ doc
45
+ end
46
+
47
+ def method_missing(m, *args, &block)
48
+ if has_view?(m)
49
+ query = args.shift || {}
50
+ return view(m, query, *args, &block)
51
+ elsif m.to_s =~ /^find_(by_.+)/
52
+ view_name = $1
53
+ if has_view?(view_name)
54
+ return first_from_view(view_name, *args)
55
+ end
56
+ end
57
+ super
58
+ end
59
+
60
+ # DocumentQueries
61
+
62
+ def all(opts = {}, &block)
63
+ docs = @klass.all({:database => @database}.merge(opts), &block)
64
+ docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs
65
+ docs
66
+ end
67
+
68
+ def count(opts = {}, &block)
69
+ @klass.all({:database => @database, :raw => true, :limit => 0}.merge(opts), &block)['total_rows']
70
+ end
71
+
72
+ def first(opts = {})
73
+ doc = @klass.first({:database => @database}.merge(opts))
74
+ doc.database = @database if doc && doc.respond_to?(:database)
75
+ doc
76
+ end
77
+
78
+ def get(id)
79
+ doc = @klass.get(id, @database)
80
+ doc.database = @database if doc && doc.respond_to?(:database)
81
+ doc
82
+ end
83
+ alias :find :get
84
+
85
+ # Views
86
+
87
+ def has_view?(view)
88
+ @klass.has_view?(view)
89
+ end
90
+
91
+ def view(name, query={}, &block)
92
+ docs = @klass.view(name, {:database => @database}.merge(query), &block)
93
+ docs.each { |doc| doc.database = @database if doc.respond_to?(:database) } if docs
94
+ docs
95
+ end
96
+
97
+ def first_from_view(name, *args)
98
+ # add to first hash available, or add to end
99
+ (args.last.is_a?(Hash) ? args.last : (args << {}).last)[:database] = @database
100
+ doc = @klass.first_from_view(name, *args)
101
+ doc.database = @database if doc && doc.respond_to?(:database)
102
+ doc
103
+ end
104
+
105
+ # DesignDoc
106
+
107
+ def design_doc
108
+ @klass.design_doc
109
+ end
110
+
111
+ def refresh_design_doc
112
+ @klass.refresh_design_doc(@database)
113
+ end
114
+
115
+ def save_design_doc
116
+ @klass.save_design_doc(@database)
117
+ end
118
+
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,263 @@
1
+ module CouchRest
2
+ module Model
3
+ module Collection
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Creates a new class method, find_all_<collection_name>, that will
12
+ # execute the view specified with the design_doc and view_name
13
+ # parameters, along with the specified view_options. This method will
14
+ # return the results of the view as an Array of objects which are
15
+ # instances of the class.
16
+ #
17
+ # This method is handy for objects that do not use the view_by method
18
+ # to declare their views.
19
+ def provides_collection(collection_name, design_doc, view_name, view_options)
20
+ class_eval <<-END, __FILE__, __LINE__ + 1
21
+ def self.find_all_#{collection_name}(options = {})
22
+ view_options = #{view_options.inspect} || {}
23
+ CollectionProxy.new(options[:database] || database, "#{design_doc}", "#{view_name}", view_options.merge(options), Kernel.const_get('#{self}'))
24
+ end
25
+ END
26
+ end
27
+
28
+ # Fetch a group of objects from CouchDB. Options can include:
29
+ # :page - Specifies the page to load (starting at 1)
30
+ # :per_page - Specifies the number of objects to load per page
31
+ #
32
+ # Defaults are used if these options are not specified.
33
+ def paginate(options)
34
+ proxy = create_collection_proxy(options)
35
+ proxy.paginate(options)
36
+ end
37
+
38
+ # Iterate over the objects in a collection, fetching them from CouchDB
39
+ # in groups. Options can include:
40
+ # :page - Specifies the page to load
41
+ # :per_page - Specifies the number of objects to load per page
42
+ #
43
+ # Defaults are used if these options are not specified.
44
+ def paginated_each(options, &block)
45
+ search = options.delete(:search)
46
+ unless search == true
47
+ proxy = create_collection_proxy(options)
48
+ else
49
+ proxy = create_search_collection_proxy(options)
50
+ end
51
+ proxy.paginated_each(options, &block)
52
+ end
53
+
54
+ # Create a CollectionProxy for the specified view and options.
55
+ # CollectionProxy behaves just like an Array, but offers support for
56
+ # pagination.
57
+ def collection_proxy_for(design_doc, view_name, view_options = {})
58
+ options = view_options.merge(:design_doc => design_doc, :view_name => view_name)
59
+ create_collection_proxy(options)
60
+ end
61
+
62
+ private
63
+
64
+ def create_collection_proxy(options)
65
+ design_doc, view_name, view_options = parse_view_options(options)
66
+ CollectionProxy.new(options[:database] || database, design_doc, view_name, view_options, self)
67
+ end
68
+
69
+ def create_search_collection_proxy(options)
70
+ design_doc, search_name, search_options = parse_search_options(options)
71
+ CollectionProxy.new(options[:database] || database, design_doc, search_name, search_options, self, :search)
72
+ end
73
+
74
+ def parse_view_options(options)
75
+ design_doc = options.delete(:design_doc)
76
+ raise ArgumentError, 'design_doc is required' if design_doc.nil?
77
+
78
+ view_name = options.delete(:view_name)
79
+ raise ArgumentError, 'view_name is required' if view_name.nil?
80
+
81
+ default_view_options = (design_doc.class == Design &&
82
+ design_doc['views'][view_name.to_s] &&
83
+ design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
84
+ view_options = default_view_options.merge(options)
85
+ view_options.delete(:database)
86
+
87
+ [design_doc, view_name, view_options]
88
+ end
89
+
90
+ def parse_search_options(options)
91
+ design_doc = options.delete(:design_doc)
92
+ raise ArgumentError, 'design_doc is required' if design_doc.nil?
93
+
94
+ search_name = options.delete(:view_name)
95
+ raise ArgumentError, 'search_name is required' if search_name.nil?
96
+
97
+ search_options = options.clone
98
+ search_options.delete(:database)
99
+
100
+ [design_doc, search_name, search_options]
101
+ end
102
+
103
+ end
104
+
105
+ class CollectionProxy
106
+ alias_method :proxy_respond_to?, :respond_to?
107
+ instance_methods.each { |m| undef_method m unless m =~ /(^__|^nil\?$|^send$|proxy_|^object_id$)/ }
108
+
109
+ DEFAULT_PAGE = 1
110
+ DEFAULT_PER_PAGE = 30
111
+
112
+ # Create a new CollectionProxy to represent the specified view. If a
113
+ # container class is specified, the proxy will create an object of the
114
+ # given type for each row that comes back from the view. If no
115
+ # container class is specified, the raw results are returned.
116
+ #
117
+ # The CollectionProxy provides support for paginating over a collection
118
+ # via the paginate, and paginated_each methods.
119
+ def initialize(database, design_doc, view_name, view_options = {}, container_class = nil, query_type = :view)
120
+ raise ArgumentError, "database is a required parameter" if database.nil?
121
+
122
+ @database = database
123
+ @container_class = container_class
124
+ @query_type = query_type
125
+
126
+ strip_pagination_options(view_options)
127
+ @view_options = view_options
128
+
129
+ if design_doc.class == Design
130
+ @view_name = "#{design_doc.name}/#{view_name}"
131
+ else
132
+ @view_name = "#{design_doc}/#{view_name}"
133
+ end
134
+ end
135
+
136
+ # See Collection.paginate
137
+ def paginate(options = {})
138
+ page, per_page = parse_options(options)
139
+ results = @database.send(@query_type, @view_name, pagination_options(page, per_page))
140
+ remember_where_we_left_off(results, page)
141
+ instances = convert_to_container_array(results)
142
+
143
+ begin
144
+ if Kernel.const_get('WillPaginate')
145
+ total_rows = results['total_rows'].to_i
146
+ paginated = WillPaginate::Collection.create(page, per_page, total_rows) do |pager|
147
+ pager.replace(instances)
148
+ end
149
+ return paginated
150
+ end
151
+ rescue NameError
152
+ # When not using will_paginate, not much we could do about this. :x
153
+ end
154
+ return instances
155
+ end
156
+
157
+ # See Collection.paginated_each
158
+ def paginated_each(options = {}, &block)
159
+ page, per_page = parse_options(options)
160
+
161
+ begin
162
+ collection = paginate({:page => page, :per_page => per_page})
163
+ collection.each(&block)
164
+ page += 1
165
+ end until collection.size < per_page
166
+ end
167
+
168
+ def respond_to?(*args)
169
+ proxy_respond_to?(*args) || (load_target && @target.respond_to?(*args))
170
+ end
171
+
172
+ # Explicitly proxy === because the instance method removal above
173
+ # doesn't catch it.
174
+ def ===(other)
175
+ load_target
176
+ other === @target
177
+ end
178
+
179
+ private
180
+
181
+ def method_missing(method, *args)
182
+ if load_target
183
+ if block_given?
184
+ @target.send(method, *args) { |*block_args| yield(*block_args) }
185
+ else
186
+ @target.send(method, *args)
187
+ end
188
+ end
189
+ end
190
+
191
+ def load_target
192
+ unless loaded?
193
+ @view_options.merge!({:include_docs => true}) if @query_type == :search
194
+ results = @database.send(@query_type, @view_name, @view_options)
195
+ @target = convert_to_container_array(results)
196
+ end
197
+ @loaded = true
198
+ @target
199
+ end
200
+
201
+ def loaded?
202
+ @loaded
203
+ end
204
+
205
+ def reload
206
+ reset
207
+ load_target
208
+ self unless @target.nil?
209
+ end
210
+
211
+ def reset
212
+ @loaded = false
213
+ @target = nil
214
+ end
215
+
216
+ def inspect
217
+ load_target
218
+ @target.inspect
219
+ end
220
+
221
+ def convert_to_container_array(results)
222
+ if @container_class.nil?
223
+ results
224
+ else
225
+ results['rows'].collect { |row| @container_class.create_from_database(row['doc']) } unless results['rows'].nil?
226
+ end
227
+ end
228
+
229
+ def pagination_options(page, per_page)
230
+ view_options = @view_options.clone
231
+ if @query_type == :view && @last_key && @last_docid && @last_page == page - 1
232
+ key = view_options.delete(:key)
233
+ end_key = view_options[:endkey] || key
234
+ options = { :startkey => @last_key, :endkey => end_key, :startkey_docid => @last_docid, :limit => per_page, :skip => 1 }
235
+ else
236
+ options = { :limit => per_page, :skip => per_page * (page - 1) }
237
+ end
238
+ view_options.merge(options)
239
+ end
240
+
241
+ def parse_options(options)
242
+ page = options.delete(:page) || DEFAULT_PAGE
243
+ per_page = options.delete(:per_page) || DEFAULT_PER_PAGE
244
+ [page.to_i, per_page.to_i]
245
+ end
246
+
247
+ def strip_pagination_options(options)
248
+ parse_options(options)
249
+ end
250
+
251
+ def remember_where_we_left_off(results, page)
252
+ last_row = results['rows'].last
253
+ if last_row
254
+ @last_key = last_row['key']
255
+ @last_docid = last_row['id']
256
+ end
257
+ @last_page = page
258
+ end
259
+ end
260
+
261
+ end
262
+ end
263
+ end
@@ -0,0 +1,51 @@
1
+ module CouchRest
2
+
3
+ # CouchRest Model Configuration support, stolen from Carrierwave by jnicklas
4
+ # http://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/uploader/configuration.rb
5
+
6
+ module Model
7
+ module Configuration
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ add_config :model_type_key
12
+ add_config :mass_assign_any_attribute
13
+
14
+ configure do |config|
15
+ config.model_type_key = 'couchrest-type' # 'model'?
16
+ config.mass_assign_any_attribute = false
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+
22
+ def add_config(name)
23
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
24
+ def self.#{name}(value=nil)
25
+ @#{name} = value if value
26
+ return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name})
27
+ name = superclass.#{name}
28
+ return nil if name.nil? && !instance_variable_defined?("@#{name}")
29
+ @#{name} = name && !name.is_a?(Module) && !name.is_a?(Symbol) && !name.is_a?(Numeric) && !name.is_a?(TrueClass) && !name.is_a?(FalseClass) ? name.dup : name
30
+ end
31
+
32
+ def self.#{name}=(value)
33
+ @#{name} = value
34
+ end
35
+
36
+ def #{name}
37
+ self.class.#{name}
38
+ end
39
+ RUBY
40
+ end
41
+
42
+ def configure
43
+ yield self
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+
51
+
@@ -0,0 +1,123 @@
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 ||= Design.new(default_design_doc)
11
+ end
12
+
13
+ # Use when something has been changed, like a view, so that on the next request
14
+ # the design docs will be updated (if changed!)
15
+ def req_design_doc_refresh
16
+ @design_doc_fresh = { }
17
+ end
18
+
19
+ def design_doc_id
20
+ "_design/#{design_doc_slug}"
21
+ end
22
+
23
+ def design_doc_slug
24
+ self.to_s
25
+ end
26
+
27
+ def default_design_doc
28
+ {
29
+ "_id" => design_doc_id,
30
+ "language" => "javascript",
31
+ "views" => {
32
+ 'all' => {
33
+ 'map' => "function(doc) {
34
+ if (doc['#{self.model_type_key}'] == '#{self.to_s}') {
35
+ emit(doc['_id'],1);
36
+ }
37
+ }"
38
+ }
39
+ }
40
+ }
41
+ end
42
+
43
+ # DEPRECATED
44
+ # use stored_design_doc to retrieve the current design doc
45
+ def all_design_doc_versions(db = database)
46
+ db.documents :startkey => "_design/#{self.to_s}",
47
+ :endkey => "_design/#{self.to_s}-\u9999"
48
+ end
49
+
50
+ # Retreive the latest version of the design document directly
51
+ # from the database.
52
+ def stored_design_doc(db = database)
53
+ db.get(design_doc_id) rescue nil
54
+ end
55
+ alias :model_design_doc :stored_design_doc
56
+
57
+ def refresh_design_doc(db = database)
58
+ raise "Database missing for design document refresh" if db.nil?
59
+ unless design_doc_fresh(db)
60
+ save_design_doc(db)
61
+ design_doc_fresh(db, true)
62
+ end
63
+ end
64
+
65
+ # Save the design doc onto a target database in a thread-safe way,
66
+ # not modifying the model's design_doc
67
+ #
68
+ # See also save_design_doc! to always save the design doc even if there
69
+ # are no changes.
70
+ def save_design_doc(db = database, force = false)
71
+ update_design_doc(Design.new(design_doc), db, force)
72
+ end
73
+
74
+ # Force the update of the model's design_doc even if it hasn't changed.
75
+ def save_design_doc!(db = database)
76
+ save_design_doc(db, true)
77
+ end
78
+
79
+ protected
80
+
81
+ def design_doc_fresh(db, fresh = nil)
82
+ @design_doc_fresh ||= {}
83
+ if fresh.nil?
84
+ @design_doc_fresh[db.uri] || false
85
+ else
86
+ @design_doc_fresh[db.uri] = fresh
87
+ end
88
+ end
89
+
90
+ # Writes out a design_doc to a given database, returning the
91
+ # updated design doc
92
+ def update_design_doc(design_doc, db, force = false)
93
+ saved = stored_design_doc(db)
94
+ if saved
95
+ changes = force
96
+ design_doc['views'].each do |name, view|
97
+ if !compare_views(saved['views'][name], view)
98
+ changes = true
99
+ saved['views'][name] = view
100
+ end
101
+ end
102
+ if changes
103
+ db.save_doc(saved)
104
+ end
105
+ design_doc
106
+ else
107
+ design_doc.database = db
108
+ design_doc.save
109
+ design_doc
110
+ end
111
+ end
112
+
113
+ # Return true if the two views match
114
+ def compare_views(orig, repl)
115
+ return false if orig.nil? or repl.nil?
116
+ (orig['map'].to_s.strip == repl['map'].to_s.strip) && (orig['reduce'].to_s.strip == repl['reduce'].to_s.strip)
117
+ end
118
+
119
+ end # module ClassMethods
120
+
121
+ end
122
+ end
123
+ end