davber_couchrest_extended_document 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 (71) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +250 -0
  3. data/Rakefile +69 -0
  4. data/THANKS.md +22 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +165 -0
  7. data/lib/couchrest/casted_array.rb +39 -0
  8. data/lib/couchrest/casted_model.rb +53 -0
  9. data/lib/couchrest/extended_document.rb +262 -0
  10. data/lib/couchrest/mixins.rb +12 -0
  11. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  12. data/lib/couchrest/mixins/attributes.rb +75 -0
  13. data/lib/couchrest/mixins/callbacks.rb +534 -0
  14. data/lib/couchrest/mixins/class_proxy.rb +120 -0
  15. data/lib/couchrest/mixins/collection.rb +260 -0
  16. data/lib/couchrest/mixins/design_doc.rb +159 -0
  17. data/lib/couchrest/mixins/document_queries.rb +82 -0
  18. data/lib/couchrest/mixins/extended_attachments.rb +73 -0
  19. data/lib/couchrest/mixins/properties.rb +130 -0
  20. data/lib/couchrest/mixins/typecast.rb +174 -0
  21. data/lib/couchrest/mixins/views.rb +148 -0
  22. data/lib/couchrest/property.rb +96 -0
  23. data/lib/couchrest/support/couchrest.rb +19 -0
  24. data/lib/couchrest/support/rails.rb +42 -0
  25. data/lib/couchrest/validation.rb +246 -0
  26. data/lib/couchrest/validation/auto_validate.rb +156 -0
  27. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  28. data/lib/couchrest/validation/validation_errors.rb +125 -0
  29. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  30. data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
  31. data/lib/couchrest/validation/validators/format_validator.rb +122 -0
  32. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  33. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  34. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  35. data/lib/couchrest/validation/validators/length_validator.rb +139 -0
  36. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  37. data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
  38. data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
  39. data/lib/couchrest_extended_document.rb +23 -0
  40. data/spec/couchrest/attribute_protection_spec.rb +150 -0
  41. data/spec/couchrest/casted_extended_doc_spec.rb +79 -0
  42. data/spec/couchrest/casted_model_spec.rb +424 -0
  43. data/spec/couchrest/extended_doc_attachment_spec.rb +148 -0
  44. data/spec/couchrest/extended_doc_inherited_spec.rb +40 -0
  45. data/spec/couchrest/extended_doc_spec.rb +869 -0
  46. data/spec/couchrest/extended_doc_subclass_spec.rb +101 -0
  47. data/spec/couchrest/extended_doc_view_spec.rb +529 -0
  48. data/spec/couchrest/property_spec.rb +790 -0
  49. data/spec/fixtures/attachments/README +3 -0
  50. data/spec/fixtures/attachments/couchdb.png +0 -0
  51. data/spec/fixtures/attachments/test.html +11 -0
  52. data/spec/fixtures/more/article.rb +35 -0
  53. data/spec/fixtures/more/card.rb +22 -0
  54. data/spec/fixtures/more/cat.rb +22 -0
  55. data/spec/fixtures/more/course.rb +25 -0
  56. data/spec/fixtures/more/event.rb +8 -0
  57. data/spec/fixtures/more/invoice.rb +17 -0
  58. data/spec/fixtures/more/person.rb +9 -0
  59. data/spec/fixtures/more/question.rb +7 -0
  60. data/spec/fixtures/more/service.rb +12 -0
  61. data/spec/fixtures/more/user.rb +22 -0
  62. data/spec/fixtures/views/lib.js +3 -0
  63. data/spec/fixtures/views/test_view/lib.js +3 -0
  64. data/spec/fixtures/views/test_view/only-map.js +4 -0
  65. data/spec/fixtures/views/test_view/test-map.js +3 -0
  66. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  67. data/spec/spec.opts +5 -0
  68. data/spec/spec_helper.rb +49 -0
  69. data/utils/remap.rb +27 -0
  70. data/utils/subset.rb +30 -0
  71. metadata +225 -0
@@ -0,0 +1,82 @@
1
+ module CouchRest
2
+ module Mixins
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 type 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 type 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 type 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
+ doc = db.get id
74
+ create_from_database(doc)
75
+ end
76
+ alias :find! :get!
77
+
78
+ end
79
+
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,73 @@
1
+ module CouchRest
2
+ module Mixins
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,130 @@
1
+ require 'time'
2
+ require File.join(File.dirname(__FILE__), '..', 'property')
3
+ require File.join(File.dirname(__FILE__), '..', 'casted_array')
4
+
5
+ module CouchRest
6
+ module Mixins
7
+ module Properties
8
+
9
+ class IncludeError < StandardError; end
10
+
11
+ def self.included(base)
12
+ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
13
+ extend CouchRest::InheritableAttributes
14
+ couchrest_inheritable_accessor(:properties) unless self.respond_to?(:properties)
15
+ self.properties ||= []
16
+ EOS
17
+ base.extend(ClassMethods)
18
+ 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?(:[]=))
19
+ end
20
+
21
+ # Returns the Class properties
22
+ #
23
+ # ==== Returns
24
+ # Array:: the list of properties for model's class
25
+ def properties
26
+ self.class.properties
27
+ end
28
+
29
+ def read_attribute(property)
30
+ self[property.to_s]
31
+ end
32
+
33
+ def write_attribute(property, value)
34
+ prop = property.is_a?(::CouchRest::Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
35
+ raise "Missing property definition for #{property.to_s}" unless prop
36
+ self[prop.to_s] = prop.cast(self, value)
37
+ end
38
+
39
+ def apply_all_property_defaults
40
+ return if self.respond_to?(:new?) && (new? == false)
41
+ # TODO: cache the default object
42
+ self.class.properties.each do |property|
43
+ write_attribute(property, property.default_value)
44
+ end
45
+ end
46
+
47
+ module ClassMethods
48
+
49
+ def property(name, *options, &block)
50
+ opts = { }
51
+ type = options.shift
52
+ if type.class != Hash
53
+ opts[:type] = type
54
+ opts.merge!(options.shift || {})
55
+ else
56
+ opts.update(type)
57
+ end
58
+ existing_property = self.properties.find{|p| p.name == name.to_s}
59
+ if existing_property.nil? || (existing_property.default != opts[:default])
60
+ define_property(name, opts, &block)
61
+ end
62
+ end
63
+
64
+ protected
65
+
66
+ # This is not a thread safe operation, if you have to set new properties at runtime
67
+ # make sure a mutex is used.
68
+ def define_property(name, options={}, &block)
69
+ # check if this property is going to casted
70
+ type = options.delete(:type) || options.delete(:cast_as)
71
+ if block_given?
72
+ type = Class.new(Hash) do
73
+ include CastedModel
74
+ end
75
+ type.class_eval { yield type }
76
+ type = [type] # inject as an array
77
+ end
78
+ property = CouchRest::Property.new(name, type, options)
79
+ create_property_getter(property)
80
+ create_property_setter(property) unless property.read_only == true
81
+ properties << property
82
+ property
83
+ end
84
+
85
+ # defines the getter for the property (and optional aliases)
86
+ def create_property_getter(property)
87
+ # meth = property.name
88
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
89
+ def #{property.name}
90
+ read_attribute('#{property.name}')
91
+ end
92
+ EOS
93
+
94
+ if ['boolean', TrueClass.to_s.downcase].include?(property.type.to_s.downcase)
95
+ class_eval <<-EOS, __FILE__, __LINE__
96
+ def #{property.name}?
97
+ value = read_attribute('#{property.name}')
98
+ !(value.nil? || value == false)
99
+ end
100
+ EOS
101
+ end
102
+
103
+ if property.alias
104
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
105
+ alias #{property.alias.to_sym} #{property.name.to_sym}
106
+ EOS
107
+ end
108
+ end
109
+
110
+ # defines the setter for the property (and optional aliases)
111
+ def create_property_setter(property)
112
+ property_name = property.name
113
+ class_eval <<-EOS
114
+ def #{property_name}=(value)
115
+ write_attribute('#{property_name}', value)
116
+ end
117
+ EOS
118
+
119
+ if property.alias
120
+ class_eval <<-EOS
121
+ alias #{property.alias.to_sym}= #{property_name.to_sym}=
122
+ EOS
123
+ end
124
+ end
125
+
126
+ end # module ClassMethods
127
+
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,174 @@
1
+ require 'time'
2
+ require 'bigdecimal'
3
+ require 'bigdecimal/util'
4
+
5
+ class Time
6
+ # returns a local time value much faster than Time.parse
7
+ def self.mktime_with_offset(string)
8
+ string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/
9
+ # $1 = year
10
+ # $2 = month
11
+ # $3 = day
12
+ # $4 = hours
13
+ # $5 = minutes
14
+ # $6 = seconds
15
+ # $7 = time zone direction
16
+ # $8 = tz difference
17
+ # utc time with wrong TZ info:
18
+ time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
19
+ tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
20
+ time + tz_difference + zone_offset(time.zone)
21
+ end
22
+ end
23
+
24
+ module CouchRest
25
+ module Mixins
26
+ module Typecast
27
+
28
+ def typecast_value(value, property) # klass, init_method)
29
+ return nil if value.nil?
30
+ klass = property.type_class
31
+ if value.instance_of?(klass) || klass == Object
32
+ value
33
+ elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
34
+ send('typecast_to_'+klass.to_s.downcase, value)
35
+ else
36
+ # Allow the init_method to be defined as a Proc for advanced conversion
37
+ property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
38
+ end
39
+ end
40
+
41
+ protected
42
+
43
+ # Typecast a value to an Integer
44
+ def typecast_to_integer(value)
45
+ typecast_to_numeric(value, :to_i)
46
+ end
47
+
48
+ # Typecast a value to a String
49
+ def typecast_to_string(value)
50
+ value.to_s
51
+ end
52
+
53
+ # Typecast a value to a true or false
54
+ def typecast_to_trueclass(value)
55
+ if value.kind_of?(Integer)
56
+ return true if value == 1
57
+ return false if value == 0
58
+ elsif value.respond_to?(:to_s)
59
+ return true if %w[ true 1 t ].include?(value.to_s.downcase)
60
+ return false if %w[ false 0 f ].include?(value.to_s.downcase)
61
+ end
62
+ value
63
+ end
64
+
65
+ # Typecast a value to a BigDecimal
66
+ def typecast_to_bigdecimal(value)
67
+ if value.kind_of?(Integer)
68
+ value.to_s.to_d
69
+ else
70
+ typecast_to_numeric(value, :to_d)
71
+ end
72
+ end
73
+
74
+ # Typecast a value to a Float
75
+ def typecast_to_float(value)
76
+ typecast_to_numeric(value, :to_f)
77
+ end
78
+
79
+ # Match numeric string
80
+ def typecast_to_numeric(value, method)
81
+ if value.respond_to?(:to_str)
82
+ if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
83
+ $1.send(method)
84
+ else
85
+ value
86
+ end
87
+ elsif value.respond_to?(method)
88
+ value.send(method)
89
+ else
90
+ value
91
+ end
92
+ end
93
+
94
+ # Typecasts an arbitrary value to a DateTime.
95
+ # Handles both Hashes and DateTime instances.
96
+ # This is slow!! Use Time instead.
97
+ def typecast_to_datetime(value)
98
+ if value.is_a?(Hash)
99
+ typecast_hash_to_datetime(value)
100
+ else
101
+ DateTime.parse(value.to_s)
102
+ end
103
+ rescue ArgumentError
104
+ value
105
+ end
106
+
107
+ # Typecasts an arbitrary value to a Date
108
+ # Handles both Hashes and Date instances.
109
+ def typecast_to_date(value)
110
+ if value.is_a?(Hash)
111
+ typecast_hash_to_date(value)
112
+ elsif value.is_a?(Time) # sometimes people think date is time!
113
+ value.to_date
114
+ elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
115
+ # Faster than parsing the date
116
+ Date.new($1.to_i, $2.to_i, $3.to_i)
117
+ else
118
+ Date.parse(value)
119
+ end
120
+ rescue ArgumentError
121
+ value
122
+ end
123
+
124
+ # Typecasts an arbitrary value to a Time
125
+ # Handles both Hashes and Time instances.
126
+ def typecast_to_time(value)
127
+ if value.is_a?(Hash)
128
+ typecast_hash_to_time(value)
129
+ else
130
+ Time.mktime_with_offset(value.to_s)
131
+ end
132
+ rescue ArgumentError
133
+ value
134
+ rescue TypeError
135
+ value
136
+ end
137
+
138
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
139
+ # :hour, :min, :sec
140
+ def typecast_hash_to_datetime(value)
141
+ DateTime.new(*extract_time(value))
142
+ end
143
+
144
+ # Creates a Date instance from a Hash with keys :year, :month, :day
145
+ def typecast_hash_to_date(value)
146
+ Date.new(*extract_time(value)[0, 3])
147
+ end
148
+
149
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
150
+ # :hour, :min, :sec
151
+ def typecast_hash_to_time(value)
152
+ Time.local(*extract_time(value))
153
+ end
154
+
155
+ # Extracts the given args from the hash. If a value does not exist, it
156
+ # uses the value of Time.now.
157
+ def extract_time(value)
158
+ now = Time.now
159
+ [:year, :month, :day, :hour, :min, :sec].map do |segment|
160
+ typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
161
+ end
162
+ end
163
+
164
+ # Typecast a value to a Class
165
+ def typecast_to_class(value)
166
+ value.to_s.constantize
167
+ rescue NameError
168
+ value
169
+ end
170
+
171
+ end
172
+ end
173
+ end
174
+