couchrest_extended_document 1.0.0.beta5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (71) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +68 -0
  3. data/Rakefile +68 -0
  4. data/THANKS.md +19 -0
  5. data/examples/model/example.rb +144 -0
  6. data/history.txt +159 -0
  7. data/lib/couchrest/casted_array.rb +25 -0
  8. data/lib/couchrest/casted_model.rb +55 -0
  9. data/lib/couchrest/extended_document.rb +323 -0
  10. data/lib/couchrest/mixins/attribute_protection.rb +74 -0
  11. data/lib/couchrest/mixins/callbacks.rb +532 -0
  12. data/lib/couchrest/mixins/class_proxy.rb +120 -0
  13. data/lib/couchrest/mixins/collection.rb +260 -0
  14. data/lib/couchrest/mixins/design_doc.rb +127 -0
  15. data/lib/couchrest/mixins/document_queries.rb +82 -0
  16. data/lib/couchrest/mixins/extended_attachments.rb +73 -0
  17. data/lib/couchrest/mixins/properties.rb +162 -0
  18. data/lib/couchrest/mixins/validation.rb +245 -0
  19. data/lib/couchrest/mixins/views.rb +148 -0
  20. data/lib/couchrest/mixins.rb +11 -0
  21. data/lib/couchrest/property.rb +50 -0
  22. data/lib/couchrest/support/couchrest.rb +19 -0
  23. data/lib/couchrest/support/rails.rb +42 -0
  24. data/lib/couchrest/typecast.rb +175 -0
  25. data/lib/couchrest/validation/auto_validate.rb +156 -0
  26. data/lib/couchrest/validation/contextual_validators.rb +78 -0
  27. data/lib/couchrest/validation/validation_errors.rb +125 -0
  28. data/lib/couchrest/validation/validators/absent_field_validator.rb +74 -0
  29. data/lib/couchrest/validation/validators/confirmation_validator.rb +107 -0
  30. data/lib/couchrest/validation/validators/format_validator.rb +122 -0
  31. data/lib/couchrest/validation/validators/formats/email.rb +66 -0
  32. data/lib/couchrest/validation/validators/formats/url.rb +43 -0
  33. data/lib/couchrest/validation/validators/generic_validator.rb +120 -0
  34. data/lib/couchrest/validation/validators/length_validator.rb +139 -0
  35. data/lib/couchrest/validation/validators/method_validator.rb +89 -0
  36. data/lib/couchrest/validation/validators/numeric_validator.rb +109 -0
  37. data/lib/couchrest/validation/validators/required_field_validator.rb +114 -0
  38. data/lib/couchrest/validation.rb +245 -0
  39. data/lib/couchrest_extended_document.rb +21 -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 +406 -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 +868 -0
  46. data/spec/couchrest/extended_doc_subclass_spec.rb +99 -0
  47. data/spec/couchrest/extended_doc_view_spec.rb +529 -0
  48. data/spec/couchrest/property_spec.rb +648 -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 +6 -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 +200 -0
@@ -0,0 +1,245 @@
1
+ # Extracted from dm-validations 0.9.10
2
+ #
3
+ # Copyright (c) 2007 Guy van den Berg
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ class Object
25
+ def validatable?
26
+ false
27
+ end
28
+ end
29
+
30
+ require 'pathname'
31
+
32
+ dir = File.join(Pathname(__FILE__).dirname.expand_path, '..', 'validation')
33
+
34
+ require File.join(dir, 'validation_errors')
35
+ require File.join(dir, 'contextual_validators')
36
+ require File.join(dir, 'auto_validate')
37
+
38
+ require File.join(dir, 'validators', 'generic_validator')
39
+ require File.join(dir, 'validators', 'required_field_validator')
40
+ require File.join(dir, 'validators', 'absent_field_validator')
41
+ require File.join(dir, 'validators', 'format_validator')
42
+ require File.join(dir, 'validators', 'length_validator')
43
+ require File.join(dir, 'validators', 'numeric_validator')
44
+ require File.join(dir, 'validators', 'method_validator')
45
+ require File.join(dir, 'validators', 'confirmation_validator')
46
+
47
+ module CouchRest
48
+ module Validation
49
+
50
+ def self.included(base)
51
+ base.extlib_inheritable_accessor(:auto_validation)
52
+ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
53
+ # Callbacks
54
+ define_callbacks :validate
55
+
56
+ # Turn off auto validation by default
57
+ self.auto_validation ||= false
58
+
59
+ # Force the auto validation for the class properties
60
+ # This feature is still not fully ported over,
61
+ # test are lacking, so please use with caution
62
+ def self.auto_validate!
63
+ self.auto_validation = true
64
+ end
65
+
66
+ # share the validations with subclasses
67
+ def self.inherited(subklass)
68
+ self.validators.contexts.each do |k, v|
69
+ subklass.validators.contexts[k] = v.dup
70
+ end
71
+ super
72
+ end
73
+ EOS
74
+
75
+ base.extend(ClassMethods)
76
+ base.class_eval <<-EOS, __FILE__, __LINE__ + 1
77
+ define_callbacks :validate
78
+ if method_defined?(:_run_save_callbacks)
79
+ set_callback :save, :before, :check_validations
80
+ end
81
+ EOS
82
+ base.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
83
+ def self.define_property(name, options={})
84
+ super
85
+ auto_generate_validations(properties.last) if properties && properties.size > 0
86
+ autovalidation_check = true
87
+ end
88
+ RUBY_EVAL
89
+ end
90
+
91
+ # Ensures the object is valid for the context provided, and otherwise
92
+ # throws :halt and returns false.
93
+ #
94
+ def check_validations(context = :default)
95
+ throw(:halt, false) unless context.nil? || valid?(context)
96
+ end
97
+
98
+ # Return the ValidationErrors
99
+ #
100
+ def errors
101
+ @errors ||= ValidationErrors.new
102
+ end
103
+
104
+ # Mark this resource as validatable. When we validate associations of a
105
+ # resource we can check if they respond to validatable? before trying to
106
+ # recursivly validate them
107
+ #
108
+ def validatable?
109
+ true
110
+ end
111
+
112
+ # Alias for valid?(:default)
113
+ #
114
+ def valid_for_default?
115
+ valid?(:default)
116
+ end
117
+
118
+ # Check if a resource is valid in a given context
119
+ #
120
+ def valid?(context = :default)
121
+ recursive_valid?(self, context, true)
122
+ end
123
+
124
+ # checking on casted objects
125
+ def validate_casted_arrays
126
+ result = true
127
+ array_casted_properties = self.class.properties.select { |property| property.casted && property.type.instance_of?(Array) }
128
+ array_casted_properties.each do |property|
129
+ casted_values = self.send(property.name)
130
+ next unless casted_values.is_a?(Array) && casted_values.first.respond_to?(:valid?)
131
+ casted_values.each do |value|
132
+ result = (result && value.valid?) if value.respond_to?(:valid?)
133
+ end
134
+ end
135
+ result
136
+ end
137
+
138
+ # Do recursive validity checking
139
+ #
140
+ def recursive_valid?(target, context, state)
141
+ valid = state
142
+ target.each do |key, prop|
143
+ if prop.is_a?(Array)
144
+ prop.each do |item|
145
+ if item.validatable?
146
+ valid = recursive_valid?(item, context, valid) && valid
147
+ end
148
+ end
149
+ elsif prop.validatable?
150
+ valid = recursive_valid?(prop, context, valid) && valid
151
+ end
152
+ end
153
+ target._run_validate_callbacks do
154
+ target.class.validators.execute(context, target) && valid
155
+ end
156
+ end
157
+
158
+
159
+ def validation_property_value(name)
160
+ self.respond_to?(name, true) ? self.send(name) : nil
161
+ end
162
+
163
+ # Get the corresponding Object property, if it exists.
164
+ def validation_property(field_name)
165
+ properties.find{|p| p.name == field_name}
166
+ end
167
+
168
+ module ClassMethods
169
+ include CouchRest::Validation::ValidatesPresent
170
+ include CouchRest::Validation::ValidatesAbsent
171
+ include CouchRest::Validation::ValidatesIsConfirmed
172
+ # include CouchRest::Validation::ValidatesIsPrimitive
173
+ # include CouchRest::Validation::ValidatesIsAccepted
174
+ include CouchRest::Validation::ValidatesFormat
175
+ include CouchRest::Validation::ValidatesLength
176
+ # include CouchRest::Validation::ValidatesWithin
177
+ include CouchRest::Validation::ValidatesIsNumber
178
+ include CouchRest::Validation::ValidatesWithMethod
179
+ # include CouchRest::Validation::ValidatesWithBlock
180
+ # include CouchRest::Validation::ValidatesIsUnique
181
+ include CouchRest::Validation::AutoValidate
182
+
183
+ # Return the set of contextual validators or create a new one
184
+ #
185
+ def validators
186
+ @validations ||= ContextualValidators.new
187
+ end
188
+
189
+ # Clean up the argument list and return a opts hash, including the
190
+ # merging of any default opts. Set the context to default if none is
191
+ # provided. Also allow :context to be aliased to :on, :when & group
192
+ #
193
+ def opts_from_validator_args(args, defaults = nil)
194
+ opts = args.last.kind_of?(Hash) ? args.pop : {}
195
+ context = :default
196
+ context = opts[:context] if opts.has_key?(:context)
197
+ context = opts.delete(:on) if opts.has_key?(:on)
198
+ context = opts.delete(:when) if opts.has_key?(:when)
199
+ context = opts.delete(:group) if opts.has_key?(:group)
200
+ opts[:context] = context
201
+ opts.merge!(defaults) unless defaults.nil?
202
+ opts
203
+ end
204
+
205
+ # Given a new context create an instance method of
206
+ # valid_for_<context>? which simply calls valid?(context)
207
+ # if it does not already exist
208
+ #
209
+ def create_context_instance_methods(context)
210
+ name = "valid_for_#{context.to_s}?" # valid_for_signup?
211
+ if !self.instance_methods.include?(name)
212
+ class_eval <<-EOS, __FILE__, __LINE__ + 1
213
+ def #{name} # def valid_for_signup?
214
+ valid?('#{context.to_s}'.to_sym) # valid?('signup'.to_sym)
215
+ end # end
216
+ EOS
217
+ end
218
+ end
219
+
220
+ # Create a new validator of the given klazz and push it onto the
221
+ # requested context for each of the attributes in the fields list
222
+ #
223
+ def add_validator_to_context(opts, fields, klazz)
224
+ fields.each do |field|
225
+ validator = klazz.new(field.to_sym, opts)
226
+ if opts[:context].is_a?(Symbol)
227
+ unless validators.context(opts[:context]).include?(validator)
228
+ validators.context(opts[:context]) << validator
229
+ create_context_instance_methods(opts[:context])
230
+ end
231
+ elsif opts[:context].is_a?(Array)
232
+ opts[:context].each do |c|
233
+ unless validators.context(c).include?(validator)
234
+ validators.context(c) << validator
235
+ create_context_instance_methods(c)
236
+ end
237
+ end
238
+ end
239
+ end
240
+ end
241
+
242
+ end # module ClassMethods
243
+ end # module Validation
244
+
245
+ end # module CouchRest
@@ -0,0 +1,148 @@
1
+ module CouchRest
2
+ module Mixins
3
+ module Views
4
+
5
+ def self.included(base)
6
+ base.extend(ClassMethods)
7
+ end
8
+
9
+ module ClassMethods
10
+ # Define a CouchDB view. The name of the view will be the concatenation
11
+ # of <tt>by</tt> and the keys joined by <tt>_and_</tt>
12
+ #
13
+ # ==== Example views:
14
+ #
15
+ # class Post
16
+ # # view with default options
17
+ # # query with Post.by_date
18
+ # view_by :date, :descending => true
19
+ #
20
+ # # view with compound sort-keys
21
+ # # query with Post.by_user_id_and_date
22
+ # view_by :user_id, :date
23
+ #
24
+ # # view with custom map/reduce functions
25
+ # # query with Post.by_tags :reduce => true
26
+ # view_by :tags,
27
+ # :map =>
28
+ # "function(doc) {
29
+ # if (doc['couchrest-type'] == 'Post' && doc.tags) {
30
+ # doc.tags.forEach(function(tag){
31
+ # emit(doc.tag, 1);
32
+ # });
33
+ # }
34
+ # }",
35
+ # :reduce =>
36
+ # "function(keys, values, rereduce) {
37
+ # return sum(values);
38
+ # }"
39
+ # end
40
+ #
41
+ # <tt>view_by :date</tt> will create a view defined by this Javascript
42
+ # function:
43
+ #
44
+ # function(doc) {
45
+ # if (doc['couchrest-type'] == 'Post' && doc.date) {
46
+ # emit(doc.date, null);
47
+ # }
48
+ # }
49
+ #
50
+ # It can be queried by calling <tt>Post.by_date</tt> which accepts all
51
+ # valid options for CouchRest::Database#view. In addition, calling with
52
+ # the <tt>:raw => true</tt> option will return the view rows
53
+ # themselves. By default <tt>Post.by_date</tt> will return the
54
+ # documents included in the generated view.
55
+ #
56
+ # Calling with :database => [instance of CouchRest::Database] will
57
+ # send the query to a specific database, otherwise it will go to
58
+ # the model's default database (use_database)
59
+ #
60
+ # CouchRest::Database#view options can be applied at view definition
61
+ # time as defaults, and they will be curried and used at view query
62
+ # time. Or they can be overridden at query time.
63
+ #
64
+ # Custom views can be queried with <tt>:reduce => true</tt> to return
65
+ # reduce results. The default for custom views is to query with
66
+ # <tt>:reduce => false</tt>.
67
+ #
68
+ # Views are generated (on a per-model basis) lazily on first-access.
69
+ # This means that if you are deploying changes to a view, the views for
70
+ # that model won't be available until generation is complete. This can
71
+ # take some time with large databases. Strategies are in the works.
72
+ #
73
+ # To understand the capabilities of this view system more completely,
74
+ # it is recommended that you read the RSpec file at
75
+ # <tt>spec/couchrest/more/extended_doc_spec.rb</tt>.
76
+
77
+ def view_by(*keys)
78
+ opts = keys.pop if keys.last.is_a?(Hash)
79
+ opts ||= {}
80
+ ducktype = opts.delete(:ducktype)
81
+ unless ducktype || opts[:map]
82
+ opts[:guards] ||= []
83
+ opts[:guards].push "(doc['couchrest-type'] == '#{self.to_s}')"
84
+ end
85
+ keys.push opts
86
+ design_doc.view_by(*keys)
87
+ req_design_doc_refresh
88
+ end
89
+
90
+ # returns stored defaults if there is a view named this in the design doc
91
+ def has_view?(view)
92
+ view = view.to_s
93
+ design_doc && design_doc['views'] && design_doc['views'][view]
94
+ end
95
+
96
+ # Dispatches to any named view.
97
+ def view(name, query={}, &block)
98
+ db = query.delete(:database) || database
99
+ refresh_design_doc(db)
100
+ query[:raw] = true if query[:reduce]
101
+ raw = query.delete(:raw)
102
+ fetch_view_with_docs(db, name, query, raw, &block)
103
+ end
104
+
105
+ private
106
+
107
+ def fetch_view_with_docs(db, name, opts, raw=false, &block)
108
+ if raw || (opts.has_key?(:include_docs) && opts[:include_docs] == false)
109
+ fetch_view(db, name, opts, &block)
110
+ else
111
+ begin
112
+ if block.nil?
113
+ collection_proxy_for(design_doc, name, opts.merge({:include_docs => true}))
114
+ else
115
+ view = fetch_view db, name, opts.merge({:include_docs => true}), &block
116
+ view['rows'].collect{|r|create_from_database(r['doc'])} if view['rows']
117
+ end
118
+ rescue
119
+ # fallback for old versions of couchdb that don't
120
+ # have include_docs support
121
+ view = fetch_view(db, name, opts, &block)
122
+ view['rows'].collect{|r|create_from_database(db.get(r['id']))} if view['rows']
123
+ end
124
+ end
125
+ end
126
+
127
+ def fetch_view(db, view_name, opts, &block)
128
+ raise "A view needs a database to operate on (specify :database option, or use_database in the #{self.class} class)" unless db
129
+ retryable = true
130
+ begin
131
+ design_doc.view_on(db, view_name, opts, &block)
132
+ # the design doc may not have been saved yet on this database
133
+ rescue RestClient::ResourceNotFound => e
134
+ if retryable
135
+ save_design_doc(db)
136
+ retryable = false
137
+ retry
138
+ else
139
+ raise e
140
+ end
141
+ end
142
+ end
143
+
144
+ end # module ClassMethods
145
+
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,11 @@
1
+ mixins_dir = File.join(File.dirname(__FILE__), 'mixins')
2
+
3
+ require File.join(mixins_dir, 'callbacks')
4
+ require File.join(mixins_dir, 'properties')
5
+ require File.join(mixins_dir, 'document_queries')
6
+ require File.join(mixins_dir, 'views')
7
+ require File.join(mixins_dir, 'design_doc')
8
+ require File.join(mixins_dir, 'extended_attachments')
9
+ require File.join(mixins_dir, 'class_proxy')
10
+ require File.join(mixins_dir, 'collection')
11
+ require File.join(mixins_dir, 'attribute_protection')
@@ -0,0 +1,50 @@
1
+ module CouchRest
2
+
3
+ # Basic attribute support for adding getter/setter + validation
4
+ class Property
5
+ attr_reader :name, :type, :read_only, :alias, :default, :casted, :init_method, :options
6
+
7
+ # attribute to define
8
+ def initialize(name, type = nil, options = {})
9
+ @name = name.to_s
10
+ parse_type(type)
11
+ parse_options(options)
12
+ self
13
+ end
14
+
15
+ private
16
+
17
+ def parse_type(type)
18
+ if type.nil?
19
+ @type = String
20
+ elsif type.is_a?(Array) && type.empty?
21
+ @type = [Object]
22
+ else
23
+ base_type = type.is_a?(Array) ? type.first : type
24
+ if base_type.is_a?(String)
25
+ if base_type.downcase == 'boolean'
26
+ base_type = TrueClass
27
+ else
28
+ begin
29
+ base_type = base_type.constantize
30
+ rescue # leave base type as a string and convert in more/typecast
31
+ end
32
+ end
33
+ end
34
+ @type = type.is_a?(Array) ? [base_type] : base_type
35
+ end
36
+ end
37
+
38
+ def parse_options(options)
39
+ return if options.empty?
40
+ @validation_format = options.delete(:format) if options[:format]
41
+ @read_only = options.delete(:read_only) if options[:read_only]
42
+ @alias = options.delete(:alias) if options[:alias]
43
+ @default = options.delete(:default) unless options[:default].nil?
44
+ @casted = options[:casted] ? true : false
45
+ @init_method = options[:init_method] ? options.delete(:init_method) : 'new'
46
+ @options = options
47
+ end
48
+
49
+ end
50
+ end
@@ -0,0 +1,19 @@
1
+
2
+ module CouchRest
3
+
4
+ class Database
5
+
6
+ alias :delete_old! :delete!
7
+ def delete!
8
+ clear_extended_doc_fresh_cache
9
+ delete_old!
10
+ end
11
+
12
+ # If the database is deleted, ensure that the design docs will be refreshed.
13
+ def clear_extended_doc_fresh_cache
14
+ ::CouchRest::ExtendedDocument.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,42 @@
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
10
+
11
+ CouchRest::Document.class_eval do
12
+ # Need this when passing doc to a resourceful route
13
+ alias_method :to_param, :id
14
+
15
+ # Hack so that CouchRest::Document, which descends from Hash,
16
+ # doesn't appear to Rails routing as a Hash of options
17
+ def is_a?(o)
18
+ return false if o == Hash
19
+ super
20
+ end
21
+ alias_method :kind_of?, :is_a?
22
+ end
23
+
24
+ CouchRest::CastedModel.class_eval do
25
+ # The to_param method is needed for rails to generate resourceful routes.
26
+ # In your controller, remember that it's actually the id of the document.
27
+ def id
28
+ return nil if base_doc.nil?
29
+ base_doc.id
30
+ end
31
+ alias_method :to_param, :id
32
+ end
33
+
34
+ require Pathname.new(File.dirname(__FILE__)).join('..', 'validation', 'validation_errors')
35
+
36
+ CouchRest::Validation::ValidationErrors.class_eval do
37
+ # Returns the total number of errors added. Two errors added to the same attribute will be counted as such.
38
+ # This method is called by error_messages_for
39
+ def count
40
+ errors.values.inject(0) { |error_count, errors_for_attribute| error_count + errors_for_attribute.size }
41
+ end
42
+ end
@@ -0,0 +1,175 @@
1
+ require 'time'
2
+ require 'bigdecimal'
3
+ require 'bigdecimal/util'
4
+ require File.join(File.dirname(__FILE__), 'property')
5
+
6
+ class Time
7
+ # returns a local time value much faster than Time.parse
8
+ def self.mktime_with_offset(string)
9
+ string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})([\+|\s|\-])*(\d{2}):?(\d{2})/
10
+ # $1 = year
11
+ # $2 = month
12
+ # $3 = day
13
+ # $4 = hours
14
+ # $5 = minutes
15
+ # $6 = seconds
16
+ # $7 = time zone direction
17
+ # $8 = tz difference
18
+ # utc time with wrong TZ info:
19
+ time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6, $7)
20
+ tz_difference = ("#{$7 == '-' ? '+' : '-'}#{$8}".to_i * 3600)
21
+ time + tz_difference + zone_offset(time.zone)
22
+ end
23
+ end
24
+
25
+ module CouchRest
26
+ module More
27
+ module Typecast
28
+
29
+ def typecast_value(value, klass, init_method)
30
+ return nil if value.nil?
31
+ klass = klass.constantize unless klass.is_a?(Class)
32
+ if value.instance_of?(klass) || klass == Object
33
+ value
34
+ elsif [String, TrueClass, Integer, Float, BigDecimal, DateTime, Time, Date, Class].include?(klass)
35
+ send('typecast_to_'+klass.to_s.downcase, value)
36
+ else
37
+ # Allow the init_method to be defined as a Proc for advanced conversion
38
+ init_method.is_a?(Proc) ? init_method.call(value) : klass.send(init_method, value)
39
+ end
40
+ end
41
+
42
+ protected
43
+
44
+ # Typecast a value to an Integer
45
+ def typecast_to_integer(value)
46
+ typecast_to_numeric(value, :to_i)
47
+ end
48
+
49
+ # Typecast a value to a String
50
+ def typecast_to_string(value)
51
+ value.to_s
52
+ end
53
+
54
+ # Typecast a value to a true or false
55
+ def typecast_to_trueclass(value)
56
+ if value.kind_of?(Integer)
57
+ return true if value == 1
58
+ return false if value == 0
59
+ elsif value.respond_to?(:to_s)
60
+ return true if %w[ true 1 t ].include?(value.to_s.downcase)
61
+ return false if %w[ false 0 f ].include?(value.to_s.downcase)
62
+ end
63
+ value
64
+ end
65
+
66
+ # Typecast a value to a BigDecimal
67
+ def typecast_to_bigdecimal(value)
68
+ if value.kind_of?(Integer)
69
+ value.to_s.to_d
70
+ else
71
+ typecast_to_numeric(value, :to_d)
72
+ end
73
+ end
74
+
75
+ # Typecast a value to a Float
76
+ def typecast_to_float(value)
77
+ typecast_to_numeric(value, :to_f)
78
+ end
79
+
80
+ # Match numeric string
81
+ def typecast_to_numeric(value, method)
82
+ if value.respond_to?(:to_str)
83
+ if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
84
+ $1.send(method)
85
+ else
86
+ value
87
+ end
88
+ elsif value.respond_to?(method)
89
+ value.send(method)
90
+ else
91
+ value
92
+ end
93
+ end
94
+
95
+ # Typecasts an arbitrary value to a DateTime.
96
+ # Handles both Hashes and DateTime instances.
97
+ # This is slow!! Use Time instead.
98
+ def typecast_to_datetime(value)
99
+ if value.is_a?(Hash)
100
+ typecast_hash_to_datetime(value)
101
+ else
102
+ DateTime.parse(value.to_s)
103
+ end
104
+ rescue ArgumentError
105
+ value
106
+ end
107
+
108
+ # Typecasts an arbitrary value to a Date
109
+ # Handles both Hashes and Date instances.
110
+ def typecast_to_date(value)
111
+ if value.is_a?(Hash)
112
+ typecast_hash_to_date(value)
113
+ elsif value.is_a?(Time) # sometimes people think date is time!
114
+ value.to_date
115
+ elsif value.to_s =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})/
116
+ # Faster than parsing the date
117
+ Date.new($1.to_i, $2.to_i, $3.to_i)
118
+ else
119
+ Date.parse(value)
120
+ end
121
+ rescue ArgumentError
122
+ value
123
+ end
124
+
125
+ # Typecasts an arbitrary value to a Time
126
+ # Handles both Hashes and Time instances.
127
+ def typecast_to_time(value)
128
+ if value.is_a?(Hash)
129
+ typecast_hash_to_time(value)
130
+ else
131
+ Time.mktime_with_offset(value.to_s)
132
+ end
133
+ rescue ArgumentError
134
+ value
135
+ rescue TypeError
136
+ value
137
+ end
138
+
139
+ # Creates a DateTime instance from a Hash with keys :year, :month, :day,
140
+ # :hour, :min, :sec
141
+ def typecast_hash_to_datetime(value)
142
+ DateTime.new(*extract_time(value))
143
+ end
144
+
145
+ # Creates a Date instance from a Hash with keys :year, :month, :day
146
+ def typecast_hash_to_date(value)
147
+ Date.new(*extract_time(value)[0, 3])
148
+ end
149
+
150
+ # Creates a Time instance from a Hash with keys :year, :month, :day,
151
+ # :hour, :min, :sec
152
+ def typecast_hash_to_time(value)
153
+ Time.local(*extract_time(value))
154
+ end
155
+
156
+ # Extracts the given args from the hash. If a value does not exist, it
157
+ # uses the value of Time.now.
158
+ def extract_time(value)
159
+ now = Time.now
160
+ [:year, :month, :day, :hour, :min, :sec].map do |segment|
161
+ typecast_to_numeric(value.fetch(segment, now.send(segment)), :to_i)
162
+ end
163
+ end
164
+
165
+ # Typecast a value to a Class
166
+ def typecast_to_class(value)
167
+ value.to_s.constantize
168
+ rescue NameError
169
+ value
170
+ end
171
+
172
+ end
173
+ end
174
+ end
175
+