openlogic-couchrest_model 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 (107) hide show
  1. data/.gitignore +11 -0
  2. data/.rspec +4 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE +176 -0
  5. data/README.md +137 -0
  6. data/Rakefile +38 -0
  7. data/THANKS.md +21 -0
  8. data/VERSION +1 -0
  9. data/benchmarks/dirty.rb +118 -0
  10. data/couchrest_model.gemspec +36 -0
  11. data/history.md +309 -0
  12. data/init.rb +1 -0
  13. data/lib/couchrest/model.rb +10 -0
  14. data/lib/couchrest/model/associations.rb +231 -0
  15. data/lib/couchrest/model/base.rb +129 -0
  16. data/lib/couchrest/model/callbacks.rb +28 -0
  17. data/lib/couchrest/model/casted_array.rb +83 -0
  18. data/lib/couchrest/model/casted_by.rb +33 -0
  19. data/lib/couchrest/model/casted_hash.rb +84 -0
  20. data/lib/couchrest/model/class_proxy.rb +135 -0
  21. data/lib/couchrest/model/collection.rb +273 -0
  22. data/lib/couchrest/model/configuration.rb +67 -0
  23. data/lib/couchrest/model/connection.rb +70 -0
  24. data/lib/couchrest/model/core_extensions/hash.rb +9 -0
  25. data/lib/couchrest/model/core_extensions/time_parsing.rb +66 -0
  26. data/lib/couchrest/model/design_doc.rb +128 -0
  27. data/lib/couchrest/model/designs.rb +91 -0
  28. data/lib/couchrest/model/designs/view.rb +513 -0
  29. data/lib/couchrest/model/dirty.rb +39 -0
  30. data/lib/couchrest/model/document_queries.rb +99 -0
  31. data/lib/couchrest/model/embeddable.rb +78 -0
  32. data/lib/couchrest/model/errors.rb +25 -0
  33. data/lib/couchrest/model/extended_attachments.rb +83 -0
  34. data/lib/couchrest/model/persistence.rb +178 -0
  35. data/lib/couchrest/model/properties.rb +228 -0
  36. data/lib/couchrest/model/property.rb +114 -0
  37. data/lib/couchrest/model/property_protection.rb +71 -0
  38. data/lib/couchrest/model/proxyable.rb +183 -0
  39. data/lib/couchrest/model/support/couchrest_database.rb +13 -0
  40. data/lib/couchrest/model/support/couchrest_design.rb +33 -0
  41. data/lib/couchrest/model/typecast.rb +154 -0
  42. data/lib/couchrest/model/validations.rb +80 -0
  43. data/lib/couchrest/model/validations/casted_model.rb +16 -0
  44. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  45. data/lib/couchrest/model/validations/uniqueness.rb +69 -0
  46. data/lib/couchrest/model/views.rb +151 -0
  47. data/lib/couchrest/railtie.rb +24 -0
  48. data/lib/couchrest_model.rb +66 -0
  49. data/lib/rails/generators/couchrest_model.rb +16 -0
  50. data/lib/rails/generators/couchrest_model/config/config_generator.rb +18 -0
  51. data/lib/rails/generators/couchrest_model/config/templates/couchdb.yml +21 -0
  52. data/lib/rails/generators/couchrest_model/model/model_generator.rb +27 -0
  53. data/lib/rails/generators/couchrest_model/model/templates/model.rb +2 -0
  54. data/spec/.gitignore +1 -0
  55. data/spec/fixtures/attachments/README +3 -0
  56. data/spec/fixtures/attachments/couchdb.png +0 -0
  57. data/spec/fixtures/attachments/test.html +11 -0
  58. data/spec/fixtures/config/couchdb.yml +10 -0
  59. data/spec/fixtures/models/article.rb +36 -0
  60. data/spec/fixtures/models/base.rb +164 -0
  61. data/spec/fixtures/models/card.rb +19 -0
  62. data/spec/fixtures/models/cat.rb +23 -0
  63. data/spec/fixtures/models/client.rb +6 -0
  64. data/spec/fixtures/models/course.rb +27 -0
  65. data/spec/fixtures/models/event.rb +8 -0
  66. data/spec/fixtures/models/invoice.rb +14 -0
  67. data/spec/fixtures/models/key_chain.rb +5 -0
  68. data/spec/fixtures/models/membership.rb +4 -0
  69. data/spec/fixtures/models/person.rb +11 -0
  70. data/spec/fixtures/models/project.rb +6 -0
  71. data/spec/fixtures/models/question.rb +7 -0
  72. data/spec/fixtures/models/sale_entry.rb +9 -0
  73. data/spec/fixtures/models/sale_invoice.rb +14 -0
  74. data/spec/fixtures/models/service.rb +10 -0
  75. data/spec/fixtures/models/user.rb +22 -0
  76. data/spec/fixtures/views/lib.js +3 -0
  77. data/spec/fixtures/views/test_view/lib.js +3 -0
  78. data/spec/fixtures/views/test_view/only-map.js +4 -0
  79. data/spec/fixtures/views/test_view/test-map.js +3 -0
  80. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  81. data/spec/functional/validations_spec.rb +8 -0
  82. data/spec/spec_helper.rb +60 -0
  83. data/spec/unit/active_model_lint_spec.rb +30 -0
  84. data/spec/unit/assocations_spec.rb +242 -0
  85. data/spec/unit/attachment_spec.rb +176 -0
  86. data/spec/unit/base_spec.rb +537 -0
  87. data/spec/unit/casted_spec.rb +72 -0
  88. data/spec/unit/class_proxy_spec.rb +167 -0
  89. data/spec/unit/collection_spec.rb +86 -0
  90. data/spec/unit/configuration_spec.rb +77 -0
  91. data/spec/unit/connection_spec.rb +148 -0
  92. data/spec/unit/core_extensions/time_parsing.rb +77 -0
  93. data/spec/unit/design_doc_spec.rb +241 -0
  94. data/spec/unit/designs/view_spec.rb +831 -0
  95. data/spec/unit/designs_spec.rb +134 -0
  96. data/spec/unit/dirty_spec.rb +436 -0
  97. data/spec/unit/embeddable_spec.rb +498 -0
  98. data/spec/unit/inherited_spec.rb +33 -0
  99. data/spec/unit/persistence_spec.rb +481 -0
  100. data/spec/unit/property_protection_spec.rb +192 -0
  101. data/spec/unit/property_spec.rb +481 -0
  102. data/spec/unit/proxyable_spec.rb +376 -0
  103. data/spec/unit/subclass_spec.rb +85 -0
  104. data/spec/unit/typecast_spec.rb +521 -0
  105. data/spec/unit/validations_spec.rb +140 -0
  106. data/spec/unit/view_spec.rb +367 -0
  107. metadata +301 -0
@@ -0,0 +1,39 @@
1
+ # encoding: utf-8
2
+
3
+ I18n.load_path << File.join(
4
+ File.dirname(__FILE__), "validations", "locale", "en.yml"
5
+ )
6
+
7
+ module CouchRest
8
+ module Model
9
+
10
+ # This applies to both Model::Base and Model::CastedModel
11
+ module Dirty
12
+ extend ActiveSupport::Concern
13
+ include ActiveModel::Dirty
14
+
15
+ included do
16
+ # internal dirty setting - overrides global setting.
17
+ # this is used to temporarily disable dirty tracking when setting
18
+ # attributes directly, for performance reasons.
19
+ self.send(:attr_accessor, :disable_dirty)
20
+ end
21
+
22
+ def use_dirty?
23
+ doc = base_doc
24
+ doc && !doc.disable_dirty
25
+ end
26
+
27
+ def couchrest_attribute_will_change!(attr)
28
+ return if attr.nil? || !use_dirty?
29
+ attribute_will_change!(attr)
30
+ couchrest_parent_will_change!
31
+ end
32
+
33
+ def couchrest_parent_will_change!
34
+ casted_by.couchrest_attribute_will_change!(casted_by_property.name) if casted_by_property
35
+ end
36
+
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,99 @@
1
+ module CouchRest
2
+ module Model
3
+ module DocumentQueries
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ # Load all documents that have the model_type_key's field equal to the
9
+ # name of the current class. Take the standard set of
10
+ # CouchRest::Database#view options.
11
+ def all(opts = {}, &block)
12
+ view(:all, opts, &block)
13
+ end
14
+
15
+ # Returns the number of documents that have the model_type_key's field
16
+ # equal to the name of the current class. Takes the standard set of
17
+ # CouchRest::Database#view options
18
+ def count(opts = {}, &block)
19
+ all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
20
+ end
21
+
22
+ # Load the first document that have the model_type_key's field equal to
23
+ # the name of the current class.
24
+ #
25
+ # ==== Returns
26
+ # Object:: The first object instance available
27
+ # or
28
+ # Nil:: if no instances available
29
+ #
30
+ # ==== Parameters
31
+ # opts<Hash>::
32
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
33
+ def first(opts = {})
34
+ first_instance = self.all(opts.merge!(:limit => 1))
35
+ first_instance.empty? ? nil : first_instance.first
36
+ end
37
+
38
+ # Load the last document that have the model_type_key's field equal to
39
+ # the name of the current class.
40
+ # It's similar to method first, just adds :descending => true
41
+ #
42
+ # ==== Returns
43
+ # Object:: The last object instance available
44
+ # or
45
+ # Nil:: if no instances available
46
+ #
47
+ # ==== Parameters
48
+ # opts<Hash>::
49
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
50
+ def last(opts = {})
51
+ first(opts.merge!(:descending => true))
52
+ end
53
+
54
+ # Load a document from the database by id
55
+ # No exceptions will be raised if the document isn't found
56
+ #
57
+ # ==== Returns
58
+ # Object:: if the document was found
59
+ # or
60
+ # Nil::
61
+ #
62
+ # === Parameters
63
+ # id<String, Integer>:: Document ID
64
+ # db<Database>:: optional option to pass a custom database to use
65
+ def get(id, db = database)
66
+ begin
67
+ get!(id, db)
68
+ rescue
69
+ nil
70
+ end
71
+ end
72
+ alias :find :get
73
+
74
+ # Load a document from the database by id
75
+ # An exception will be raised if the document isn't found
76
+ #
77
+ # ==== Returns
78
+ # Object:: if the document was found
79
+ # or
80
+ # Exception
81
+ #
82
+ # === Parameters
83
+ # id<String, Integer>:: Document ID
84
+ # db<Database>:: optional option to pass a custom database to use
85
+ def get!(id, db = database)
86
+ raise CouchRest::Model::DocumentNotFound if id.blank?
87
+
88
+ doc = db.get id
89
+ build_from_database(doc)
90
+ rescue RestClient::ResourceNotFound
91
+ raise CouchRest::Model::DocumentNotFound
92
+ end
93
+ alias :find! :get!
94
+
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,78 @@
1
+ module CouchRest::Model
2
+ module Embeddable
3
+ extend ActiveSupport::Concern
4
+
5
+ # Include Attributes early to ensure super() will work
6
+ include CouchRest::Attributes
7
+
8
+ included do
9
+ include CouchRest::Model::Configuration
10
+ include CouchRest::Model::Properties
11
+ include CouchRest::Model::PropertyProtection
12
+ include CouchRest::Model::Associations
13
+ include CouchRest::Model::Validations
14
+ include CouchRest::Model::Callbacks
15
+ include CouchRest::Model::CastedBy
16
+ include CouchRest::Model::Dirty
17
+ include CouchRest::Model::Callbacks
18
+
19
+ class_eval do
20
+ # Override CastedBy's base_doc?
21
+ def base_doc?
22
+ false # Can never be base doc!
23
+ end
24
+
25
+ end
26
+ end
27
+
28
+ # Initialize a new Casted Model. Accepts the same
29
+ # options as CouchRest::Model::Base for preparing and initializing
30
+ # attributes.
31
+ def initialize(keys = {}, options = {})
32
+ super()
33
+ prepare_all_attributes(keys, options)
34
+ run_callbacks(:initialize) { self }
35
+ end
36
+
37
+ # False if the casted model has already
38
+ # been saved in the containing document
39
+ def new?
40
+ casted_by.nil? ? true : casted_by.new?
41
+ end
42
+ alias :new_record? :new?
43
+
44
+ def persisted?
45
+ !new?
46
+ end
47
+
48
+ # The to_param method is needed for rails to generate resourceful routes.
49
+ # In your controller, remember that it's actually the id of the document.
50
+ def id
51
+ return nil if base_doc.nil?
52
+ base_doc.id
53
+ end
54
+ alias :to_key :id
55
+ alias :to_param :id
56
+
57
+ # Sets the attributes from a hash
58
+ def update_attributes_without_saving(hash)
59
+ hash.each do |k, v|
60
+ raise NoMethodError, "#{k}= method not available, use property :#{k}" unless self.respond_to?("#{k}=")
61
+ end
62
+ hash.each do |k, v|
63
+ self.send("#{k}=",v)
64
+ end
65
+ end
66
+ alias :attributes= :update_attributes_without_saving
67
+
68
+ end # End Embeddable
69
+
70
+ # Provide backwards compatability with previous versions (pre 1.1.0)
71
+ module CastedModel
72
+ extend ActiveSupport::Concern
73
+ included do
74
+ include CouchRest::Model::Embeddable
75
+ end
76
+ end
77
+
78
+ end
@@ -0,0 +1,25 @@
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
+
23
+ class DocumentNotFound < Errors::CouchRestModelError; end
24
+ end
25
+ end
@@ -0,0 +1,83 @@
1
+ module CouchRest
2
+ module Model
3
+ module ExtendedAttachments
4
+ extend ActiveSupport::Concern
5
+
6
+ # Add a file attachment to the current document. Expects
7
+ # :file and :name to be included in the arguments.
8
+ def create_attachment(args={})
9
+ raise ArgumentError unless args[:file] && args[:name]
10
+ return if has_attachment?(args[:name])
11
+ set_attachment_attr(args)
12
+ rescue ArgumentError => e
13
+ raise ArgumentError, 'You must specify :file and :name'
14
+ end
15
+
16
+ # return all attachments
17
+ def attachments
18
+ self['_attachments'] ||= {}
19
+ end
20
+
21
+ # reads the data from an attachment
22
+ def read_attachment(attachment_name)
23
+ database.fetch_attachment(self, attachment_name)
24
+ end
25
+
26
+ # modifies a file attachment on the current doc
27
+ def update_attachment(args={})
28
+ raise ArgumentError unless args[:file] && args[:name]
29
+ return unless has_attachment?(args[:name])
30
+ delete_attachment(args[:name])
31
+ set_attachment_attr(args)
32
+ rescue ArgumentError => e
33
+ raise ArgumentError, 'You must specify :file and :name'
34
+ end
35
+
36
+ # deletes a file attachment from the current doc
37
+ def delete_attachment(attachment_name)
38
+ return unless attachments
39
+ if attachments.include?(attachment_name)
40
+ attribute_will_change!("_attachments")
41
+ attachments.delete attachment_name
42
+ end
43
+ end
44
+
45
+ # returns true if attachment_name exists
46
+ def has_attachment?(attachment_name)
47
+ !!(attachments && attachments[attachment_name] && !attachments[attachment_name].empty?)
48
+ end
49
+
50
+ # returns URL to fetch the attachment from
51
+ def attachment_url(attachment_name)
52
+ return unless has_attachment?(attachment_name)
53
+ "#{database.root}/#{self.id}/#{attachment_name}"
54
+ end
55
+
56
+ # returns URI to fetch the attachment from
57
+ def attachment_uri(attachment_name)
58
+ return unless has_attachment?(attachment_name)
59
+ "#{database.uri}/#{self.id}/#{attachment_name}"
60
+ end
61
+
62
+ private
63
+
64
+ def get_mime_type(path)
65
+ return nil if path.nil?
66
+ type = ::MIME::Types.type_for(path)
67
+ type.empty? ? nil : type.first.content_type
68
+ end
69
+
70
+ def set_attachment_attr(args)
71
+ content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
72
+ content_type ||= (get_mime_type(args[:name]) || 'text/plain')
73
+
74
+ attribute_will_change!("_attachments")
75
+ attachments[args[:name]] = {
76
+ 'content_type' => content_type,
77
+ 'data' => args[:file].read
78
+ }
79
+ end
80
+
81
+ end # module ExtendedAttachments
82
+ end
83
+ end
@@ -0,0 +1,178 @@
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
+ ret = (result["ok"] == true) ? self : false
16
+ @changed_attributes.clear if ret && @changed_attributes
17
+ ret
18
+ end
19
+ end
20
+ end
21
+
22
+ # Creates the document in the db. Raises an exception
23
+ # if the document is not created properly.
24
+ def create!(options = {})
25
+ self.class.fail_validate!(self) unless self.create(options)
26
+ end
27
+
28
+ # Trigger the callbacks (before, after, around)
29
+ # only if the document isn't new
30
+ def update(options = {})
31
+ raise "Cannot save a destroyed document!" if destroyed?
32
+ raise "Calling #{self.class.name}#update on document that has not been created!" if new?
33
+ return false unless perform_validations(options)
34
+ return true if !self.disable_dirty && !self.changed?
35
+ _run_update_callbacks do
36
+ _run_save_callbacks do
37
+ result = database.save_doc(self)
38
+ ret = result["ok"] == true
39
+ @changed_attributes.clear if ret && @changed_attributes
40
+ ret
41
+ end
42
+ end
43
+ end
44
+
45
+ # Trigger the callbacks (before, after, around) and save the document
46
+ def save(options = {})
47
+ self.new? ? create(options) : update(options)
48
+ end
49
+
50
+ # Saves the document to the db using save. Raises an exception
51
+ # if the document is not saved properly.
52
+ def save!
53
+ self.class.fail_validate!(self) unless self.save
54
+ true
55
+ end
56
+
57
+ # Deletes the document from the database. Runs the :destroy callbacks.
58
+ def destroy
59
+ _run_destroy_callbacks do
60
+ result = database.delete_doc(self)
61
+ if result['ok']
62
+ @_destroyed = true
63
+ self.freeze
64
+ end
65
+ result['ok']
66
+ end
67
+ end
68
+
69
+ def destroyed?
70
+ !!@_destroyed
71
+ end
72
+
73
+ def persisted?
74
+ !new? && !destroyed?
75
+ end
76
+
77
+ # Update the document's attributes and save. For example:
78
+ #
79
+ # doc.update_attributes :name => "Fred"
80
+ # Is the equivilent of doing the following:
81
+ #
82
+ # doc.attributes = { :name => "Fred" }
83
+ # doc.save
84
+ #
85
+ def update_attributes(hash)
86
+ update_attributes_without_saving hash
87
+ save
88
+ end
89
+
90
+ # Reloads the attributes of this object from the database.
91
+ # It doesn't override custom instance variables.
92
+ #
93
+ # Returns self.
94
+ def reload
95
+ prepare_all_attributes(database.get(id), :directly_set_attributes => true)
96
+ self
97
+ end
98
+
99
+ protected
100
+
101
+ def perform_validations(options = {})
102
+ perform_validation = case options
103
+ when Hash
104
+ options[:validate] != false
105
+ else
106
+ options
107
+ end
108
+ perform_validation ? valid? : true
109
+ end
110
+
111
+
112
+ module ClassMethods
113
+
114
+ # Creates a new instance, bypassing attribute protection and
115
+ # uses the type field to determine which model to use to instanatiate
116
+ # the new object.
117
+ #
118
+ # ==== Returns
119
+ # a document instance
120
+ #
121
+ def build_from_database(doc = {}, options = {}, &block)
122
+ src = doc[model_type_key]
123
+ base = (src.blank? || src == self.to_s) ? self : src.constantize
124
+ base.new(doc, options.merge(:directly_set_attributes => true), &block)
125
+ end
126
+
127
+ # Defines an instance and save it directly to the database
128
+ #
129
+ # ==== Returns
130
+ # returns the reloaded document
131
+ def create(attributes = {}, &block)
132
+ instance = new(attributes, &block)
133
+ instance.create
134
+ instance
135
+ end
136
+
137
+ # Defines an instance and save it directly to the database
138
+ #
139
+ # ==== Returns
140
+ # returns the reloaded document or raises an exception
141
+ def create!(attributes = {}, &block)
142
+ instance = new(attributes, &block)
143
+ instance.create!
144
+ instance
145
+ end
146
+
147
+ # Name a method that will be called before the document is first saved,
148
+ # which returns a string to be used for the document's <tt>_id</tt>.
149
+ #
150
+ # Because CouchDB enforces a constraint that each id must be unique,
151
+ # this can be used to enforce eg: uniq usernames. Note that this id
152
+ # must be globally unique across all document types which share a
153
+ # database, so if you'd like to scope uniqueness to this class, you
154
+ # should use the class name as part of the unique id.
155
+ def unique_id(method = nil, &block)
156
+ if method
157
+ define_method :set_unique_id do
158
+ self['_id'] ||= self.send(method)
159
+ end
160
+ elsif block
161
+ define_method :set_unique_id do
162
+ uniqid = block.call(self)
163
+ raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
164
+ self['_id'] ||= uniqid
165
+ end
166
+ end
167
+ end
168
+
169
+ # Raise an error if validation failed.
170
+ def fail_validate!(document)
171
+ raise Errors::Validations.new(document)
172
+ end
173
+ end
174
+
175
+
176
+ end
177
+ end
178
+ end