couchrest_model_thought 1.0.0.beta8

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 (70) hide show
  1. data/Gemfile +9 -0
  2. data/LICENSE +176 -0
  3. data/README.md +320 -0
  4. data/Rakefile +69 -0
  5. data/THANKS.md +19 -0
  6. data/examples/model/example.rb +144 -0
  7. data/lib/couchrest/model/associations.rb +207 -0
  8. data/lib/couchrest/model/attribute_protection.rb +74 -0
  9. data/lib/couchrest/model/attributes.rb +91 -0
  10. data/lib/couchrest/model/base.rb +111 -0
  11. data/lib/couchrest/model/callbacks.rb +27 -0
  12. data/lib/couchrest/model/casted_array.rb +39 -0
  13. data/lib/couchrest/model/casted_model.rb +68 -0
  14. data/lib/couchrest/model/class_proxy.rb +122 -0
  15. data/lib/couchrest/model/collection.rb +260 -0
  16. data/lib/couchrest/model/design_doc.rb +126 -0
  17. data/lib/couchrest/model/document_queries.rb +82 -0
  18. data/lib/couchrest/model/errors.rb +23 -0
  19. data/lib/couchrest/model/extended_attachments.rb +73 -0
  20. data/lib/couchrest/model/persistence.rb +141 -0
  21. data/lib/couchrest/model/properties.rb +144 -0
  22. data/lib/couchrest/model/property.rb +96 -0
  23. data/lib/couchrest/model/support/couchrest.rb +19 -0
  24. data/lib/couchrest/model/support/hash.rb +9 -0
  25. data/lib/couchrest/model/typecast.rb +170 -0
  26. data/lib/couchrest/model/validations/casted_model.rb +14 -0
  27. data/lib/couchrest/model/validations/locale/en.yml +5 -0
  28. data/lib/couchrest/model/validations/uniqueness.rb +42 -0
  29. data/lib/couchrest/model/validations.rb +68 -0
  30. data/lib/couchrest/model/version.rb +5 -0
  31. data/lib/couchrest/model/views.rb +160 -0
  32. data/lib/couchrest/model.rb +5 -0
  33. data/lib/couchrest_model.rb +53 -0
  34. data/spec/couchrest/assocations_spec.rb +213 -0
  35. data/spec/couchrest/attachment_spec.rb +148 -0
  36. data/spec/couchrest/attribute_protection_spec.rb +153 -0
  37. data/spec/couchrest/base_spec.rb +463 -0
  38. data/spec/couchrest/casted_model_spec.rb +424 -0
  39. data/spec/couchrest/casted_spec.rb +75 -0
  40. data/spec/couchrest/class_proxy_spec.rb +132 -0
  41. data/spec/couchrest/inherited_spec.rb +40 -0
  42. data/spec/couchrest/persistence_spec.rb +409 -0
  43. data/spec/couchrest/property_spec.rb +804 -0
  44. data/spec/couchrest/subclass_spec.rb +99 -0
  45. data/spec/couchrest/validations.rb +85 -0
  46. data/spec/couchrest/view_spec.rb +463 -0
  47. data/spec/fixtures/attachments/README +3 -0
  48. data/spec/fixtures/attachments/couchdb.png +0 -0
  49. data/spec/fixtures/attachments/test.html +11 -0
  50. data/spec/fixtures/base.rb +139 -0
  51. data/spec/fixtures/more/article.rb +35 -0
  52. data/spec/fixtures/more/card.rb +17 -0
  53. data/spec/fixtures/more/cat.rb +19 -0
  54. data/spec/fixtures/more/course.rb +25 -0
  55. data/spec/fixtures/more/event.rb +8 -0
  56. data/spec/fixtures/more/invoice.rb +14 -0
  57. data/spec/fixtures/more/person.rb +9 -0
  58. data/spec/fixtures/more/question.rb +7 -0
  59. data/spec/fixtures/more/service.rb +10 -0
  60. data/spec/fixtures/more/user.rb +22 -0
  61. data/spec/fixtures/views/lib.js +3 -0
  62. data/spec/fixtures/views/test_view/lib.js +3 -0
  63. data/spec/fixtures/views/test_view/only-map.js +4 -0
  64. data/spec/fixtures/views/test_view/test-map.js +3 -0
  65. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  66. data/spec/spec.opts +5 -0
  67. data/spec/spec_helper.rb +48 -0
  68. data/utils/remap.rb +27 -0
  69. data/utils/subset.rb +30 -0
  70. metadata +215 -0
@@ -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
@@ -0,0 +1,19 @@
1
+
2
+ module CouchRest
3
+
4
+ class Database
5
+
6
+ alias :delete_orig! :delete!
7
+ def delete!
8
+ clear_model_fresh_cache
9
+ delete_orig!
10
+ end
11
+
12
+ # If the database is deleted, ensure that the design docs will be refreshed.
13
+ def clear_model_fresh_cache
14
+ ::CouchRest::Model::Base.subclasses.each{|klass| klass.req_design_doc_refresh if klass.respond_to?(:req_design_doc_refresh)}
15
+ end
16
+
17
+ end
18
+
19
+ end
@@ -0,0 +1,9 @@
1
+ # This file contains various hacks for Rails compatibility.
2
+ class Hash
3
+ # Hack so that CouchRest::Document, which descends from Hash,
4
+ # doesn't appear to Rails routing as a Hash of options
5
+ def self.===(other)
6
+ return false if self == Hash && other.is_a?(CouchRest::Document)
7
+ super
8
+ end
9
+ end
@@ -0,0 +1,170 @@
1
+ class Time
2
+ # returns a local time value much faster than Time.parse
3
+ def self.mktime_with_offset(string)
4
+ string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/
5
+ # $1 = year
6
+ # $2 = month
7
+ # $3 = day
8
+ # $4 = hours
9
+ # $5 = minutes
10
+ # $6 = seconds
11
+ # $7 = time zone direction
12
+ # $8 = tz difference
13
+ # utc time with wrong TZ info:
14
+ time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
15
+ tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
16
+ time + tz_difference + zone_offset(time.zone)
17
+ end
18
+ end
19
+
20
+ module CouchRest
21
+ module Model
22
+ module Typecast
23
+
24
+ def typecast_value(value, property) # klass, init_method)
25
+ return nil if value.nil?
26
+ klass = property.type_class
27
+ if value.instance_of?(klass) || klass == Object
28
+ value
29
+ elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
30
+ send('typecast_to_'+klass.to_s.downcase, value)
31
+ else
32
+ # Allow the init_method to be defined as a Proc for advanced conversion
33
+ property.init_method.is_a?(Proc) ? property.init_method.call(value) : klass.send(property.init_method, value)
34
+ end
35
+ end
36
+
37
+ protected
38
+
39
+ # Typecast a value to an Integer
40
+ def typecast_to_integer(value)
41
+ typecast_to_numeric(value, :to_i)
42
+ end
43
+
44
+ # Typecast a value to a String
45
+ def typecast_to_string(value)
46
+ value.to_s
47
+ end
48
+
49
+ # Typecast a value to a true or false
50
+ def typecast_to_trueclass(value)
51
+ if value.kind_of?(Integer)
52
+ return true if value == 1
53
+ return false if value == 0
54
+ elsif value.respond_to?(:to_s)
55
+ return true if %w[ true 1 t ].include?(value.to_s.downcase)
56
+ return false if %w[ false 0 f ].include?(value.to_s.downcase)
57
+ end
58
+ value
59
+ end
60
+
61
+ # Typecast a value to a BigDecimal
62
+ def typecast_to_bigdecimal(value)
63
+ if value.kind_of?(Integer)
64
+ value.to_s.to_d
65
+ else
66
+ typecast_to_numeric(value, :to_d)
67
+ end
68
+ end
69
+
70
+ # Typecast a value to a Float
71
+ def typecast_to_float(value)
72
+ typecast_to_numeric(value, :to_f)
73
+ end
74
+
75
+ # Match numeric string
76
+ def typecast_to_numeric(value, method)
77
+ if value.respond_to?(:to_str)
78
+ if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
79
+ $1.send(method)
80
+ else
81
+ value
82
+ end
83
+ elsif value.respond_to?(method)
84
+ value.send(method)
85
+ else
86
+ value
87
+ end
88
+ end
89
+
90
+ # Typecasts an arbitrary value to a DateTime.
91
+ # Handles both Hashes and DateTime instances.
92
+ # This is slow!! Use Time instead.
93
+ def typecast_to_datetime(value)
94
+ if value.is_a?(Hash)
95
+ typecast_hash_to_datetime(value)
96
+ else
97
+ DateTime.parse(value.to_s)
98
+ end
99
+ rescue ArgumentError
100
+ value
101
+ end
102
+
103
+ # Typecasts an arbitrary value to a Date
104
+ # Handles both Hashes and Date instances.
105
+ def typecast_to_date(value)
106
+ if value.is_a?(Hash)
107
+ typecast_hash_to_date(value)
108
+ elsif value.is_a?(Time) # sometimes people think date is time!
109
+ value.to_date
110
+ elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
111
+ # Faster than parsing the date
112
+ Date.new($1.to_i, $2.to_i, $3.to_i)
113
+ else
114
+ Date.parse(value)
115
+ end
116
+ rescue ArgumentError
117
+ value
118
+ end
119
+
120
+ # Typecasts an arbitrary value to a Time
121
+ # Handles both Hashes and Time instances.
122
+ def typecast_to_time(value)
123
+ if value.is_a?(Hash)
124
+ typecast_hash_to_time(value)
125
+ else
126
+ Time.mktime_with_offset(value.to_s)
127
+ end
128
+ rescue ArgumentError
129
+ value
130
+ rescue TypeError
131
+ value
132
+ end
133
+
134
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
135
+ # :hour, :min, :sec
136
+ def typecast_hash_to_datetime(value)
137
+ DateTime.new(*extract_time(value))
138
+ end
139
+
140
+ # Creates a Date instance from a Hash with keys :year, :month, :day
141
+ def typecast_hash_to_date(value)
142
+ Date.new(*extract_time(value)[0, 3])
143
+ end
144
+
145
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
146
+ # :hour, :min, :sec
147
+ def typecast_hash_to_time(value)
148
+ Time.local(*extract_time(value))
149
+ end
150
+
151
+ # Extracts the given args from the hash. If a value does not exist, it
152
+ # uses the value of Time.now.
153
+ def extract_time(value)
154
+ now = Time.now
155
+ [:year, :month, :day, :hour, :min, :sec].map do |segment|
156
+ typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
157
+ end
158
+ end
159
+
160
+ # Typecast a value to a Class
161
+ def typecast_to_class(value)
162
+ value.to_s.constantize
163
+ rescue NameError
164
+ value
165
+ end
166
+
167
+ end
168
+ end
169
+ end
170
+
@@ -0,0 +1,14 @@
1
+ module CouchRest
2
+ module Model
3
+ module Validations
4
+ class CastedModelValidator < ActiveModel::EachValidator
5
+
6
+ def validate_each(document, attribute, value)
7
+ values = value.is_a?(Array) ? value : [value]
8
+ return if values.collect {|doc| doc.nil? || doc.valid? }.all?
9
+ document.errors.add(attribute, :invalid, :default => options[:message], :value => value)
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,5 @@
1
+ en:
2
+ errors:
3
+ messages:
4
+ taken: "is already taken"
5
+