couchrest_model 1.0.0.beta7

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