couchrest_model 1.0.0.beta7

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 (69) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +320 -0
  3. data/Rakefile +71 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +180 -0
  7. data/lib/couchrest/model.rb +10 -0
  8. data/lib/couchrest/model/associations.rb +207 -0
  9. data/lib/couchrest/model/attribute_protection.rb +74 -0
  10. data/lib/couchrest/model/attributes.rb +75 -0
  11. data/lib/couchrest/model/base.rb +111 -0
  12. data/lib/couchrest/model/callbacks.rb +27 -0
  13. data/lib/couchrest/model/casted_array.rb +39 -0
  14. data/lib/couchrest/model/casted_model.rb +68 -0
  15. data/lib/couchrest/model/class_proxy.rb +122 -0
  16. data/lib/couchrest/model/collection.rb +260 -0
  17. data/lib/couchrest/model/design_doc.rb +126 -0
  18. data/lib/couchrest/model/document_queries.rb +82 -0
  19. data/lib/couchrest/model/errors.rb +23 -0
  20. data/lib/couchrest/model/extended_attachments.rb +73 -0
  21. data/lib/couchrest/model/persistence.rb +141 -0
  22. data/lib/couchrest/model/properties.rb +144 -0
  23. data/lib/couchrest/model/property.rb +96 -0
  24. data/lib/couchrest/model/support/couchrest.rb +19 -0
  25. data/lib/couchrest/model/support/hash.rb +9 -0
  26. data/lib/couchrest/model/typecast.rb +170 -0
  27. data/lib/couchrest/model/validations.rb +68 -0
  28. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  29. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  30. data/lib/couchrest/model/validations/uniqueness.rb +45 -0
  31. data/lib/couchrest/model/views.rb +167 -0
  32. data/lib/couchrest_model.rb +56 -0
  33. data/spec/couchrest/assocations_spec.rb +213 -0
  34. data/spec/couchrest/attachment_spec.rb +148 -0
  35. data/spec/couchrest/attribute_protection_spec.rb +153 -0
  36. data/spec/couchrest/base_spec.rb +463 -0
  37. data/spec/couchrest/casted_model_spec.rb +424 -0
  38. data/spec/couchrest/casted_spec.rb +75 -0
  39. data/spec/couchrest/class_proxy_spec.rb +132 -0
  40. data/spec/couchrest/inherited_spec.rb +40 -0
  41. data/spec/couchrest/persistence_spec.rb +409 -0
  42. data/spec/couchrest/property_spec.rb +804 -0
  43. data/spec/couchrest/subclass_spec.rb +99 -0
  44. data/spec/couchrest/validations.rb +73 -0
  45. data/spec/couchrest/view_spec.rb +463 -0
  46. data/spec/fixtures/attachments/README +3 -0
  47. data/spec/fixtures/attachments/couchdb.png +0 -0
  48. data/spec/fixtures/attachments/test.html +11 -0
  49. data/spec/fixtures/base.rb +139 -0
  50. data/spec/fixtures/more/article.rb +35 -0
  51. data/spec/fixtures/more/card.rb +17 -0
  52. data/spec/fixtures/more/cat.rb +19 -0
  53. data/spec/fixtures/more/course.rb +25 -0
  54. data/spec/fixtures/more/event.rb +8 -0
  55. data/spec/fixtures/more/invoice.rb +14 -0
  56. data/spec/fixtures/more/person.rb +9 -0
  57. data/spec/fixtures/more/question.rb +7 -0
  58. data/spec/fixtures/more/service.rb +10 -0
  59. data/spec/fixtures/more/user.rb +22 -0
  60. data/spec/fixtures/views/lib.js +3 -0
  61. data/spec/fixtures/views/test_view/lib.js +3 -0
  62. data/spec/fixtures/views/test_view/only-map.js +4 -0
  63. data/spec/fixtures/views/test_view/test-map.js +3 -0
  64. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  65. data/spec/spec.opts +5 -0
  66. data/spec/spec_helper.rb +48 -0
  67. data/utils/remap.rb +27 -0
  68. data/utils/subset.rb +30 -0
  69. metadata +232 -0
@@ -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,73 @@
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
+ self['_attachments'] ||= {}
11
+ set_attachment_attr(args)
12
+ rescue ArgumentError => e
13
+ raise ArgumentError, 'You must specify :file and :name'
14
+ end
15
+
16
+ # reads the data from an attachment
17
+ def read_attachment(attachment_name)
18
+ database.fetch_attachment(self, attachment_name)
19
+ end
20
+
21
+ # modifies a file attachment on the current doc
22
+ def update_attachment(args={})
23
+ raise ArgumentError unless args[:file] && args[:name]
24
+ return unless has_attachment?(args[:name])
25
+ delete_attachment(args[:name])
26
+ set_attachment_attr(args)
27
+ rescue ArgumentError => e
28
+ raise ArgumentError, 'You must specify :file and :name'
29
+ end
30
+
31
+ # deletes a file attachment from the current doc
32
+ def delete_attachment(attachment_name)
33
+ return unless self['_attachments']
34
+ self['_attachments'].delete attachment_name
35
+ end
36
+
37
+ # returns true if attachment_name exists
38
+ def has_attachment?(attachment_name)
39
+ !!(self['_attachments'] && self['_attachments'][attachment_name] && !self['_attachments'][attachment_name].empty?)
40
+ end
41
+
42
+ # returns URL to fetch the attachment from
43
+ def attachment_url(attachment_name)
44
+ return unless has_attachment?(attachment_name)
45
+ "#{database.root}/#{self.id}/#{attachment_name}"
46
+ end
47
+
48
+ # returns URI to fetch the attachment from
49
+ def attachment_uri(attachment_name)
50
+ return unless has_attachment?(attachment_name)
51
+ "#{database.uri}/#{self.id}/#{attachment_name}"
52
+ end
53
+
54
+ private
55
+
56
+ def get_mime_type(path)
57
+ return nil if path.nil?
58
+ type = ::MIME::Types.type_for(path)
59
+ type.empty? ? nil : type.first.content_type
60
+ end
61
+
62
+ def set_attachment_attr(args)
63
+ content_type = args[:content_type] ? args[:content_type] : get_mime_type(args[:file].path)
64
+ content_type ||= (get_mime_type(args[:name]) || 'text/plain')
65
+ self['_attachments'][args[:name]] = {
66
+ 'content_type' => content_type,
67
+ 'data' => args[:file].read
68
+ }
69
+ end
70
+
71
+ end # module ExtendedAttachments
72
+ end
73
+ end
@@ -0,0 +1,141 @@
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
+ protected
66
+
67
+ def perform_validations(options = {})
68
+ perform_validation = case options
69
+ when Hash
70
+ options[:validate] != false
71
+ else
72
+ options
73
+ end
74
+ perform_validation ? valid? : true
75
+ end
76
+
77
+
78
+ module ClassMethods
79
+
80
+ # Creates a new instance, bypassing attribute protection
81
+ #
82
+ #
83
+ # ==== Returns
84
+ # a document instance
85
+ def create_from_database(doc = {})
86
+ base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize
87
+ base.new(doc, :directly_set_attributes => true)
88
+ end
89
+
90
+ # Defines an instance and save it directly to the database
91
+ #
92
+ # ==== Returns
93
+ # returns the reloaded document
94
+ def create(attributes = {})
95
+ instance = new(attributes)
96
+ instance.create
97
+ instance
98
+ end
99
+
100
+ # Defines an instance and save it directly to the database
101
+ #
102
+ # ==== Returns
103
+ # returns the reloaded document or raises an exception
104
+ def create!(attributes = {})
105
+ instance = new(attributes)
106
+ instance.create!
107
+ instance
108
+ end
109
+
110
+ # Name a method that will be called before the document is first saved,
111
+ # which returns a string to be used for the document's <tt>_id</tt>.
112
+ #
113
+ # Because CouchDB enforces a constraint that each id must be unique,
114
+ # this can be used to enforce eg: uniq usernames. Note that this id
115
+ # must be globally unique across all document types which share a
116
+ # database, so if you'd like to scope uniqueness to this class, you
117
+ # should use the class name as part of the unique id.
118
+ def unique_id method = nil, &block
119
+ if method
120
+ define_method :set_unique_id do
121
+ self['_id'] ||= self.send(method)
122
+ end
123
+ elsif block
124
+ define_method :set_unique_id do
125
+ uniqid = block.call(self)
126
+ raise ArgumentError, "unique_id block must not return nil" if uniqid.nil?
127
+ self['_id'] ||= uniqid
128
+ end
129
+ end
130
+ end
131
+
132
+ # Raise an error if validation failed.
133
+ def fail_validate!(document)
134
+ raise Errors::Validations.new(document)
135
+ end
136
+ end
137
+
138
+
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,144 @@
1
+ # encoding: utf-8
2
+ module CouchRest
3
+ module Model
4
+ module Properties
5
+
6
+ class IncludeError < StandardError; end
7
+
8
+ def self.included(base)
9
+ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
10
+ extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
11
+ self.properties ||= []
12
+ EOS
13
+ base.extend(ClassMethods)
14
+ raise CouchRest::Mixins::Properties::IncludeError, "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 (base.new.respond_to?(:[]) && base.new.respond_to?(:[]=))
15
+ end
16
+
17
+ # Returns the Class properties
18
+ #
19
+ # ==== Returns
20
+ # Array:: the list of properties for model's class
21
+ def properties
22
+ self.class.properties
23
+ end
24
+
25
+ def read_attribute(property)
26
+ self[property.to_s]
27
+ end
28
+
29
+ def write_attribute(property, value)
30
+ prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
31
+ raise "Missing property definition for #{property.to_s}" unless prop
32
+ self[prop.to_s] = prop.cast(self, value)
33
+ end
34
+
35
+ def apply_all_property_defaults
36
+ return if self.respond_to?(:new?) && (new? == false)
37
+ # TODO: cache the default object
38
+ self.class.properties.each do |property|
39
+ write_attribute(property, property.default_value)
40
+ end
41
+ end
42
+
43
+ module ClassMethods
44
+
45
+ def property(name, *options, &block)
46
+ opts = { }
47
+ type = options.shift
48
+ if type.class != Hash
49
+ opts[:type] = type
50
+ opts.merge!(options.shift || {})
51
+ else
52
+ opts.update(type)
53
+ end
54
+ existing_property = self.properties.find{|p| p.name == name.to_s}
55
+ if existing_property.nil? || (existing_property.default != opts[:default])
56
+ define_property(name, opts, &block)
57
+ end
58
+ end
59
+
60
+ # Automatically set <tt>updated_at</tt> and <tt>created_at</tt> fields
61
+ # on the document whenever saving occurs. CouchRest uses a pretty
62
+ # decent time format by default. See Time#to_json
63
+ def timestamps!
64
+ class_eval <<-EOS, __FILE__, __LINE__
65
+ property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
66
+ property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
67
+
68
+ set_callback :save, :before do |object|
69
+ write_attribute('updated_at', Time.now)
70
+ write_attribute('created_at', Time.now) if object.new?
71
+ end
72
+ EOS
73
+ end
74
+
75
+ protected
76
+
77
+ # This is not a thread safe operation, if you have to set new properties at runtime
78
+ # make sure a mutex is used.
79
+ def define_property(name, options={}, &block)
80
+ # check if this property is going to casted
81
+ type = options.delete(:type) || options.delete(:cast_as)
82
+ if block_given?
83
+ type = Class.new(Hash) do
84
+ include CastedModel
85
+ end
86
+ type.class_eval { yield type }
87
+ type = [type] # inject as an array
88
+ end
89
+ property = Property.new(name, type, options)
90
+ create_property_getter(property)
91
+ create_property_setter(property) unless property.read_only == true
92
+ if property.type_class.respond_to?(:validates_casted_model)
93
+ validates_casted_model property.name
94
+ end
95
+ properties << property
96
+ property
97
+ end
98
+
99
+ # defines the getter for the property (and optional aliases)
100
+ def create_property_getter(property)
101
+ # meth = property.name
102
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
103
+ def #{property.name}
104
+ read_attribute('#{property.name}')
105
+ end
106
+ EOS
107
+
108
+ if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
109
+ class_eval <<-EOS, __FILE__, __LINE__
110
+ def #{property.name}?
111
+ value = read_attribute('#{property.name}')
112
+ !(value.nil? || value == false)
113
+ end
114
+ EOS
115
+ end
116
+
117
+ if property.alias
118
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
119
+ alias #{property.alias.to_sym} #{property.name.to_sym}
120
+ EOS
121
+ end
122
+ end
123
+
124
+ # defines the setter for the property (and optional aliases)
125
+ def create_property_setter(property)
126
+ property_name = property.name
127
+ class_eval <<-EOS
128
+ def #{property_name}=(value)
129
+ write_attribute('#{property_name}', value)
130
+ end
131
+ EOS
132
+
133
+ if property.alias
134
+ class_eval <<-EOS
135
+ alias #{property.alias.to_sym}= #{property_name.to_sym}=
136
+ EOS
137
+ end
138
+ end
139
+
140
+ end # module ClassMethods
141
+
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,96 @@
1
+ # encoding: utf-8
2
+ module CouchRest::Model
3
+ class Property
4
+
5
+ include ::CouchRest::Model::Typecast
6
+
7
+ attr_reader :name, :type, :type_class, :read_only, :alias, :default, :casted, :init_method, :options
8
+
9
+ # Attribute to define.
10
+ # All Properties are assumed casted unless the type is nil.
11
+ def initialize(name, type = nil, options = {})
12
+ @name = name.to_s
13
+ @casted = true
14
+ parse_type(type)
15
+ parse_options(options)
16
+ self
17
+ end
18
+
19
+ def to_s
20
+ name
21
+ end
22
+
23
+ # Cast the provided value using the properties details.
24
+ def cast(parent, value)
25
+ return value unless casted
26
+ if type.is_a?(Array)
27
+ if value.nil?
28
+ value = []
29
+ elsif [Hash, HashWithIndifferentAccess].include?(value.class)
30
+ # Assume provided as a Hash where key is index!
31
+ data = value
32
+ value = [ ]
33
+ data.keys.sort.each do |k|
34
+ value << data[k]
35
+ end
36
+ elsif value.class != Array
37
+ raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}"
38
+ end
39
+ arr = value.collect { |data| cast_value(parent, data) }
40
+ # allow casted_by calls to be passed up chain by wrapping in CastedArray
41
+ value = type_class != String ? CastedArray.new(arr, self) : arr
42
+ value.casted_by = parent if value.respond_to?(:casted_by)
43
+ elsif !value.nil?
44
+ value = cast_value(parent, value)
45
+ end
46
+ value
47
+ end
48
+
49
+ # Cast an individual value, not an array
50
+ def cast_value(parent, value)
51
+ raise "An array inside an array cannot be casted, use CastedModel" if value.is_a?(Array)
52
+ value = typecast_value(value, self)
53
+ associate_casted_value_to_parent(parent, value)
54
+ end
55
+
56
+ def default_value
57
+ return if default.nil?
58
+ if default.class == Proc
59
+ default.call
60
+ else
61
+ Marshal.load(Marshal.dump(default))
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def associate_casted_value_to_parent(parent, value)
68
+ value.casted_by = parent if value.respond_to?(:casted_by)
69
+ value
70
+ end
71
+
72
+ def parse_type(type)
73
+ if type.nil?
74
+ @casted = false
75
+ @type = nil
76
+ @type_class = nil
77
+ else
78
+ base = type.is_a?(Array) ? type.first : type
79
+ base = Object if base.nil?
80
+ raise "Defining a property type as a #{type.class.name.humanize} is not supported in CouchRest Model!" if base.class != Class
81
+ @type_class = base
82
+ @type = type
83
+ end
84
+ end
85
+
86
+ def parse_options(options)
87
+ @validation_format = options.delete(:format) if options[:format]
88
+ @read_only = options.delete(:read_only) if options[:read_only]
89
+ @alias = options.delete(:alias) if options[:alias]
90
+ @default = options.delete(:default) unless options[:default].nil?
91
+ @init_method = options[:init_method] ? options.delete(:init_method) : 'new'
92
+ @options = options
93
+ end
94
+
95
+ end
96
+ end