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