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,83 @@
1
+ module CouchRest
2
+ module Model
3
+ module DocumentQueries
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+
11
+ # Load all documents that have the model_type_key's field equal to the
12
+ # name of the current class. Take the standard set of
13
+ # CouchRest::Database#view options.
14
+ def all(opts = {}, &block)
15
+ view(:all, opts, &block)
16
+ end
17
+
18
+ # Returns the number of documents that have the model_type_key's field
19
+ # equal to the name of the current class. Takes the standard set of
20
+ # CouchRest::Database#view options
21
+ def count(opts = {}, &block)
22
+ all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
23
+ end
24
+
25
+ # Load the first document that have the model_type_key's field equal to
26
+ # the name of the current class.
27
+ #
28
+ # ==== Returns
29
+ # Object:: The first object instance available
30
+ # or
31
+ # Nil:: if no instances available
32
+ #
33
+ # ==== Parameters
34
+ # opts<Hash>::
35
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
36
+ def first(opts = {})
37
+ first_instance = self.all(opts.merge!(:limit => 1))
38
+ first_instance.empty? ? nil : first_instance.first
39
+ end
40
+
41
+ # Load a document from the database by id
42
+ # No exceptions will be raised if the document isn't found
43
+ #
44
+ # ==== Returns
45
+ # Object:: if the document was found
46
+ # or
47
+ # Nil::
48
+ #
49
+ # === Parameters
50
+ # id<String, Integer>:: Document ID
51
+ # db<Database>:: optional option to pass a custom database to use
52
+ def get(id, db = database)
53
+ begin
54
+ get!(id, db)
55
+ rescue
56
+ nil
57
+ end
58
+ end
59
+ alias :find :get
60
+
61
+ # Load a document from the database by id
62
+ # An exception will be raised if the document isn't found
63
+ #
64
+ # ==== Returns
65
+ # Object:: if the document was found
66
+ # or
67
+ # Exception
68
+ #
69
+ # === Parameters
70
+ # id<String, Integer>:: Document ID
71
+ # db<Database>:: optional option to pass a custom database to use
72
+ def get!(id, db = database)
73
+ raise "Missing or empty document ID" if id.to_s.empty?
74
+ doc = db.get id
75
+ create_from_database(doc)
76
+ end
77
+ alias :find! :get!
78
+
79
+ end
80
+
81
+ end
82
+ end
83
+ end
@@ -0,0 +1,23 @@
1
+ # encoding: utf-8
2
+ module CouchRest
3
+ module Model
4
+ module Errors
5
+
6
+ class CouchRestModelError < StandardError; end
7
+
8
+ # Raised when a persisence method ending in ! fails validation. The message
9
+ # will contain the full error messages from the +Document+ in question.
10
+ #
11
+ # Example:
12
+ #
13
+ # <tt>Validations.new(person.errors)</tt>
14
+ class Validations < CouchRestModelError
15
+ attr_reader :document
16
+ def initialize(document)
17
+ @document = document
18
+ super("Validation Failed: #{@document.errors.full_messages.join(", ")}")
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,77 @@
1
+ module CouchRest
2
+ module Model
3
+ module ExtendedAttachments
4
+
5
+ # Add a file attachment to the current document. Expects
6
+ # :file and :name to be included in the arguments.
7
+ def create_attachment(args={})
8
+ raise ArgumentError unless args[:file] && args[:name]
9
+ return if has_attachment?(args[:name])
10
+ set_attachment_attr(args)
11
+ rescue ArgumentError => e
12
+ raise ArgumentError, 'You must specify :file and :name'
13
+ end
14
+
15
+ # return all attachments
16
+ def attachments
17
+ self['_attachments'] ||= {}
18
+ end
19
+
20
+ # reads the data from an attachment
21
+ def read_attachment(attachment_name)
22
+ database.fetch_attachment(self, attachment_name)
23
+ end
24
+
25
+ # modifies a file attachment on the current doc
26
+ def update_attachment(args={})
27
+ raise ArgumentError unless args[:file] && args[:name]
28
+ return unless has_attachment?(args[:name])
29
+ delete_attachment(args[:name])
30
+ set_attachment_attr(args)
31
+ rescue ArgumentError => e
32
+ raise ArgumentError, 'You must specify :file and :name'
33
+ end
34
+
35
+ # deletes a file attachment from the current doc
36
+ def delete_attachment(attachment_name)
37
+ return unless attachments
38
+ attachments.delete attachment_name
39
+ end
40
+
41
+ # returns true if attachment_name exists
42
+ def has_attachment?(attachment_name)
43
+ !!(attachments && attachments[attachment_name] && !attachments[attachment_name].empty?)
44
+ end
45
+
46
+ # returns URL to fetch the attachment from
47
+ def attachment_url(attachment_name)
48
+ return unless has_attachment?(attachment_name)
49
+ "#{database.root}/#{self.id}/#{attachment_name}"
50
+ end
51
+
52
+ # returns URI to fetch the attachment from
53
+ def attachment_uri(attachment_name)
54
+ return unless has_attachment?(attachment_name)
55
+ "#{database.uri}/#{self.id}/#{attachment_name}"
56
+ end
57
+
58
+ private
59
+
60
+ def get_mime_type(path)
61
+ return nil if path.nil?
62
+ type = ::MIME::Types.type_for(path)
63
+ type.empty? ? nil : type.first.content_type
64
+ end
65
+
66
+ def set_attachment_attr(args)
67
+ content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
68
+ content_type ||= (get_mime_type(args[:name]) || 'text/plain')
69
+ attachments[args[:name]] = {
70
+ 'content_type' => content_type,
71
+ 'data' => args[:file].read
72
+ }
73
+ end
74
+
75
+ end # module ExtendedAttachments
76
+ end
77
+ end
@@ -0,0 +1,155 @@
1
+ module CouchRest
2
+ module Model
3
+ module Persistence
4
+ extend ActiveSupport::Concern
5
+
6
+ # Create the document. Validation is enabled by default and will return
7
+ # false if the document is not valid. If all goes well, the document will
8
+ # be returned.
9
+ def create(options = {})
10
+ return false unless perform_validations(options)
11
+ _run_create_callbacks do
12
+ _run_save_callbacks do
13
+ set_unique_id if new? && self.respond_to?(:set_unique_id)
14
+ result = database.save_doc(self)
15
+ (result["ok"] == true) ? self : false
16
+ end
17
+ end
18
+ end
19
+
20
+ # Creates the document in the db. Raises an exception
21
+ # if the document is not created properly.
22
+ def create!
23
+ self.class.fail_validate!(self) unless self.create
24
+ end
25
+
26
+ # Trigger the callbacks (before, after, around)
27
+ # only if the document isn't new
28
+ def update(options = {})
29
+ raise "Calling #{self.class.name}#update on document that has not been created!" if self.new?
30
+ return false unless perform_validations(options)
31
+ _run_update_callbacks do
32
+ _run_save_callbacks do
33
+ result = database.save_doc(self)
34
+ result["ok"] == true
35
+ end
36
+ end
37
+ end
38
+
39
+ # Trigger the callbacks (before, after, around) and save the document
40
+ def save(options = {})
41
+ self.new? ? create(options) : update(options)
42
+ end
43
+
44
+ # Saves the document to the db using save. Raises an exception
45
+ # if the document is not saved properly.
46
+ def save!
47
+ self.class.fail_validate!(self) unless self.save
48
+ true
49
+ end
50
+
51
+ # Deletes the document from the database. Runs the :destroy callbacks.
52
+ # Removes the <tt>_id</tt> and <tt>_rev</tt> fields, preparing the
53
+ # document to be saved to a new <tt>_id</tt> if required.
54
+ def destroy
55
+ _run_destroy_callbacks do
56
+ result = database.delete_doc(self)
57
+ if result['ok']
58
+ self.delete('_rev')
59
+ self.delete('_id')
60
+ end
61
+ result['ok']
62
+ end
63
+ end
64
+
65
+ # Update the document's attributes and save. For example:
66
+ #
67
+ # doc.update_attributes :name => "Fred"
68
+ #
69
+ # Is the equivilent of doing the following:
70
+ #
71
+ # doc.attributes = { :name => "Fred" }
72
+ # doc.save
73
+ #
74
+ def update_attributes(hash)
75
+ update_attributes_without_saving hash
76
+ save
77
+ end
78
+
79
+ protected
80
+
81
+ def perform_validations(options = {})
82
+ perform_validation = case options
83
+ when Hash
84
+ options[:validate] != false
85
+ else
86
+ options
87
+ end
88
+ perform_validation ? valid? : true
89
+ end
90
+
91
+
92
+ module ClassMethods
93
+
94
+ # Creates a new instance, bypassing attribute protection
95
+ #
96
+ #
97
+ # ==== Returns
98
+ # a document instance
99
+ def create_from_database(doc = {})
100
+ base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
101
+ base.new(doc, :directly_set_attributes => true)
102
+ end
103
+
104
+ # Defines an instance and save it directly to the database
105
+ #
106
+ # ==== Returns
107
+ # returns the reloaded document
108
+ def create(attributes = {})
109
+ instance = new(attributes)
110
+ instance.create
111
+ instance
112
+ end
113
+
114
+ # Defines an instance and save it directly to the database
115
+ #
116
+ # ==== Returns
117
+ # returns the reloaded document or raises an exception
118
+ def create!(attributes = {})
119
+ instance = new(attributes)
120
+ instance.create!
121
+ instance
122
+ end
123
+
124
+ # Name a method that will be called before the document is first saved,
125
+ # which returns a string to be used for the document's <tt>_id</tt>.
126
+ #
127
+ # Because CouchDB enforces a constraint that each id must be unique,
128
+ # this can be used to enforce eg: uniq usernames. Note that this id
129
+ # must be globally unique across all document types which share a
130
+ # database, so if you'd like to scope uniqueness to this class, you
131
+ # should use the class name as part of the unique id.
132
+ def unique_id method = nil, &block
133
+ if method
134
+ define_method :set_unique_id do
135
+ self['_id'] ||= self.send(method)
136
+ end
137
+ elsif block
138
+ define_method :set_unique_id do
139
+ uniqid = block.call(self)
140
+ raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
141
+ self['_id'] ||= uniqid
142
+ end
143
+ end
144
+ end
145
+
146
+ # Raise an error if validation failed.
147
+ def fail_validate!(document)
148
+ raise Errors::Validations.new(document)
149
+ end
150
+ end
151
+
152
+
153
+ end
154
+ end
155
+ end
@@ -0,0 +1,208 @@
1
+ # encoding: utf-8
2
+ module CouchRest
3
+ module Model
4
+ module Properties
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
9
+ self.properties ||= []
10
+ raise "You can only mixin Properties in a class responding to [] and []=, if you tried to mixin CastedModel, make sure your class inherits from Hash or responds to the proper methods" unless (method_defined?(:[]) && method_defined?(:[]=))
11
+ end
12
+
13
+ # Returns the Class properties
14
+ #
15
+ # ==== Returns
16
+ # Array:: the list of properties for model's class
17
+ def properties
18
+ self.class.properties
19
+ end
20
+
21
+ # Read the casted value of an attribute defined with a property.
22
+ #
23
+ # ==== Returns
24
+ # Object:: the casted attibutes value.
25
+ def read_attribute(property)
26
+ self[find_property!(property).to_s]
27
+ end
28
+
29
+ # Store a casted value in the current instance of an attribute defined
30
+ # with a property.
31
+ def write_attribute(property, value)
32
+ prop = find_property!(property)
33
+ self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
34
+ end
35
+
36
+ # Takes a hash as argument, and applies the values by using writer methods
37
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
38
+ # missing. In case of error, no attributes are changed.
39
+ def update_attributes_without_saving(hash)
40
+ # Remove any protected and update all the rest. Any attributes
41
+ # which do not have a property will simply be ignored.
42
+ attrs = remove_protected_attributes(hash)
43
+ directly_set_attributes(attrs)
44
+ end
45
+ alias :attributes= :update_attributes_without_saving
46
+
47
+
48
+ private
49
+ # The following methods should be accessable by the Model::Base Class, but not by anything else!
50
+
51
+ def apply_all_property_defaults
52
+ return if self.respond_to?(:new?) && (new? == false)
53
+ # TODO: cache the default object
54
+ self.class.properties.each do |property|
55
+ write_attribute(property, property.default_value)
56
+ end
57
+ end
58
+
59
+ def prepare_all_attributes(doc = {}, options = {})
60
+ apply_all_property_defaults
61
+ if options[:directly_set_attributes]
62
+ directly_set_read_only_attributes(doc)
63
+ else
64
+ doc = remove_protected_attributes(doc)
65
+ end
66
+ directly_set_attributes(doc) unless doc.nil?
67
+ end
68
+
69
+ def find_property!(property)
70
+ prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
71
+ raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
72
+ prop
73
+ end
74
+
75
+ # Set all the attributes and return a hash with the attributes
76
+ # that have not been accepted.
77
+ def directly_set_attributes(hash)
78
+ hash.reject do |attribute_name, attribute_value|
79
+ if self.respond_to?("#{attribute_name}=")
80
+ self.send("#{attribute_name}=", attribute_value)
81
+ true
82
+ elsif mass_assign_any_attribute # config option
83
+ self[attribute_name] = attribute_value
84
+ true
85
+ else
86
+ false
87
+ end
88
+ end
89
+ end
90
+
91
+ def directly_set_read_only_attributes(hash)
92
+ property_list = self.properties.map{|p| p.name}
93
+ hash.each do |attribute_name, attribute_value|
94
+ next if self.respond_to?("#{attribute_name}=")
95
+ if property_list.include?(attribute_name)
96
+ write_attribute(attribute_name, hash.delete(attribute_name))
97
+ end
98
+ end
99
+ end
100
+
101
+ def set_attributes(hash)
102
+ attrs = remove_protected_attributes(hash)
103
+ directly_set_attributes(attrs)
104
+ end
105
+
106
+
107
+ module ClassMethods
108
+
109
+ def property(name, *options, &block)
110
+ opts = { }
111
+ type = options.shift
112
+ if type.class != Hash
113
+ opts[:type] = type
114
+ opts.merge!(options.shift || {})
115
+ else
116
+ opts.update(type)
117
+ end
118
+ existing_property = self.properties.find{|p| p.name == name.to_s}
119
+ if existing_property.nil? || (existing_property.default != opts[:default])
120
+ define_property(name, opts, &block)
121
+ end
122
+ end
123
+
124
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
125
+ # on the document whenever saving occurs. CouchRest uses a pretty
126
+ # decent time format by default. See Time#to_json
127
+ def timestamps!
128
+ class_eval <<-EOS, __FILE__, __LINE__
129
+ property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
130
+ property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
131
+
132
+ set_callback :save, :before do |object|
133
+ write_attribute('updated_at', Time.now)
134
+ write_attribute('created_at', Time.now) if object.new?
135
+ end
136
+ EOS
137
+ end
138
+
139
+ protected
140
+
141
+ # This is not a thread safe operation, if you have to set new properties at runtime
142
+ # make sure a mutex is used.
143
+ def define_property(name, options={}, &block)
144
+ # check if this property is going to casted
145
+ type = options.delete(:type) || options.delete(:cast_as)
146
+ if block_given?
147
+ type = Class.new(Hash) do
148
+ include CastedModel
149
+ end
150
+ type.class_eval { yield type }
151
+ type = [type] # inject as an array
152
+ end
153
+ property = Property.new(name, type, options)
154
+ create_property_getter(property)
155
+ create_property_setter(property) unless property.read_only == true
156
+ if property.type_class.respond_to?(:validates_casted_model)
157
+ validates_casted_model property.name
158
+ end
159
+ properties << property
160
+ property
161
+ end
162
+
163
+ # defines the getter for the property (and optional aliases)
164
+ def create_property_getter(property)
165
+ # meth = property.name
166
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
167
+ def #{property.name}
168
+ read_attribute('#{property.name}')
169
+ end
170
+ EOS
171
+
172
+ if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
173
+ class_eval <<-EOS, __FILE__, __LINE__
174
+ def #{property.name}?
175
+ value = read_attribute('#{property.name}')
176
+ !(value.nil? || value == false)
177
+ end
178
+ EOS
179
+ end
180
+
181
+ if property.alias
182
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
183
+ alias #{property.alias.to_sym} #{property.name.to_sym}
184
+ EOS
185
+ end
186
+ end
187
+
188
+ # defines the setter for the property (and optional aliases)
189
+ def create_property_setter(property)
190
+ property_name = property.name
191
+ class_eval <<-EOS
192
+ def #{property_name}=(value)
193
+ write_attribute('#{property_name}', value)
194
+ end
195
+ EOS
196
+
197
+ if property.alias
198
+ class_eval <<-EOS
199
+ alias #{property.alias.to_sym}= #{property_name.to_sym}=
200
+ EOS
201
+ end
202
+ end
203
+
204
+ end # module ClassMethods
205
+
206
+ end
207
+ end
208
+ end