couchrest_model-radiant 1.0.0

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