openlogic-couchrest_model 1.0.0

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