couchrest_model 1.0.0.beta8 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +9 -0
  2. data/{spec/spec.opts → .rspec} +0 -1
  3. data/Gemfile +4 -0
  4. data/Gemfile.lock +77 -0
  5. data/README.md +144 -57
  6. data/Rakefile +12 -43
  7. data/VERSION +1 -0
  8. data/couchrest_model.gemspec +35 -0
  9. data/history.txt +23 -1
  10. data/init.rb +1 -0
  11. data/lib/couchrest/model/associations.rb +17 -1
  12. data/lib/couchrest/model/base.rb +5 -5
  13. data/lib/couchrest/model/casted_model.rb +2 -2
  14. data/lib/couchrest/model/class_proxy.rb +6 -0
  15. data/lib/couchrest/model/collection.rb +3 -0
  16. data/lib/couchrest/model/configuration.rb +51 -0
  17. data/lib/couchrest/model/design_doc.rb +2 -5
  18. data/lib/couchrest/model/document_queries.rb +20 -3
  19. data/lib/couchrest/model/persistence.rb +15 -1
  20. data/lib/couchrest/model/properties.rb +97 -23
  21. data/lib/couchrest/model/property.rb +5 -4
  22. data/lib/couchrest/model/property_protection.rb +71 -0
  23. data/lib/couchrest/model/typecast.rb +12 -7
  24. data/lib/couchrest/model/view.rb +190 -0
  25. data/lib/couchrest/model/views.rb +3 -3
  26. data/lib/couchrest/model.rb +1 -1
  27. data/lib/couchrest/railtie.rb +3 -2
  28. data/lib/couchrest_model.rb +7 -14
  29. data/lib/rails/generators/couchrest_model/model/model_generator.rb +2 -1
  30. data/spec/.gitignore +1 -0
  31. data/spec/couchrest/base_spec.rb +3 -3
  32. data/spec/couchrest/casted_model_spec.rb +63 -49
  33. data/spec/couchrest/class_proxy_spec.rb +6 -0
  34. data/spec/couchrest/configuration_spec.rb +78 -0
  35. data/spec/couchrest/persistence_spec.rb +10 -4
  36. data/spec/couchrest/{attribute_protection_spec.rb → property_protection_spec.rb} +29 -2
  37. data/spec/couchrest/property_spec.rb +61 -0
  38. data/spec/couchrest/subclass_spec.rb +2 -2
  39. data/spec/couchrest/view_spec.rb +6 -0
  40. data/spec/fixtures/more/article.rb +1 -1
  41. data/spec/spec_helper.rb +4 -3
  42. metadata +96 -32
  43. data/lib/couchrest/model/attribute_protection.rb +0 -74
  44. data/lib/couchrest/model/attributes.rb +0 -75
@@ -4,6 +4,7 @@ module CouchRest
4
4
 
5
5
  extend ActiveModel::Naming
6
6
 
7
+ include CouchRest::Model::Configuration
7
8
  include CouchRest::Model::Persistence
8
9
  include CouchRest::Model::Callbacks
9
10
  include CouchRest::Model::DocumentQueries
@@ -12,8 +13,7 @@ module CouchRest
12
13
  include CouchRest::Model::ExtendedAttachments
13
14
  include CouchRest::Model::ClassProxy
14
15
  include CouchRest::Model::Collection
15
- include CouchRest::Model::AttributeProtection
16
- include CouchRest::Model::Attributes
16
+ include CouchRest::Model::PropertyProtection
17
17
  include CouchRest::Model::Associations
18
18
  include CouchRest::Model::Validations
19
19
 
@@ -37,7 +37,7 @@ module CouchRest
37
37
 
38
38
  # Accessors
39
39
  attr_accessor :casted_by
40
-
40
+
41
41
 
42
42
  # Instantiate a new CouchRest::Model::Base by preparing all properties
43
43
  # using the provided document hash.
@@ -47,10 +47,10 @@ module CouchRest
47
47
  # * :directly_set_attributes: true when data comes directly from database
48
48
  #
49
49
  def initialize(doc = {}, options = {})
50
- prepare_all_attributes(doc, options)
50
+ doc = prepare_all_attributes(doc, options)
51
51
  super(doc)
52
52
  unless self['_id'] && self['_rev']
53
- self['couchrest-type'] = self.class.to_s
53
+ self[self.model_type_key] = self.class.to_s
54
54
  end
55
55
  after_initialize if respond_to?(:after_initialize)
56
56
  end
@@ -4,10 +4,10 @@ module CouchRest::Model
4
4
  extend ActiveSupport::Concern
5
5
 
6
6
  included do
7
- include CouchRest::Model::AttributeProtection
8
- include CouchRest::Model::Attributes
7
+ include CouchRest::Model::Configuration
9
8
  include CouchRest::Model::Callbacks
10
9
  include CouchRest::Model::Properties
10
+ include CouchRest::Model::PropertyProtection
11
11
  include CouchRest::Model::Associations
12
12
  include CouchRest::Model::Validations
13
13
  attr_accessor :casted_by
@@ -75,6 +75,12 @@ module CouchRest
75
75
  doc
76
76
  end
77
77
 
78
+ def last(opts = {})
79
+ doc = @klass.last({:database => @database}.merge(opts))
80
+ doc.database = @database if doc && doc.respond_to?(:database)
81
+ doc
82
+ end
83
+
78
84
  def get(id)
79
85
  doc = @klass.get(id, @database)
80
86
  doc.database = @database if doc && doc.respond_to?(:database)
@@ -82,6 +82,7 @@ module CouchRest
82
82
  design_doc['views'][view_name.to_s] &&
83
83
  design_doc['views'][view_name.to_s]["couchrest-defaults"]) || {}
84
84
  view_options = default_view_options.merge(options)
85
+ view_options.delete(:database)
85
86
 
86
87
  [design_doc, view_name, view_options]
87
88
  end
@@ -94,6 +95,8 @@ module CouchRest
94
95
  raise ArgumentError, 'search_name is required' if search_name.nil?
95
96
 
96
97
  search_options = options.clone
98
+ search_options.delete(:database)
99
+
97
100
  [design_doc, search_name, search_options]
98
101
  end
99
102
 
@@ -0,0 +1,51 @@
1
+ module CouchRest
2
+
3
+ # CouchRest Model Configuration support, stolen from Carrierwave by jnicklas
4
+ # http://github.com/jnicklas/carrierwave/blob/master/lib/carrierwave/uploader/configuration.rb
5
+
6
+ module Model
7
+ module Configuration
8
+ extend ActiveSupport::Concern
9
+
10
+ included do
11
+ add_config :model_type_key
12
+ add_config :mass_assign_any_attribute
13
+
14
+ configure do |config|
15
+ config.model_type_key = 'couchrest-type' # 'model'?
16
+ config.mass_assign_any_attribute = false
17
+ end
18
+ end
19
+
20
+ module ClassMethods
21
+
22
+ def add_config(name)
23
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
24
+ def self.#{name}(value=nil)
25
+ @#{name} = value if value
26
+ return @#{name} if self.object_id == #{self.object_id} || defined?(@#{name})
27
+ name = superclass.#{name}
28
+ return nil if name.nil? && !instance_variable_defined?("@#{name}")
29
+ @#{name} = name && !name.is_a?(Module) && !name.is_a?(Symbol) && !name.is_a?(Numeric) && !name.is_a?(TrueClass) && !name.is_a?(FalseClass) ? name.dup : name
30
+ end
31
+
32
+ def self.#{name}=(value)
33
+ @#{name} = value
34
+ end
35
+
36
+ def #{name}
37
+ self.class.#{name}
38
+ end
39
+ RUBY
40
+ end
41
+
42
+ def configure
43
+ yield self
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
50
+
51
+
@@ -2,10 +2,7 @@
2
2
  module CouchRest
3
3
  module Model
4
4
  module DesignDoc
5
-
6
- def self.included(base)
7
- base.extend(ClassMethods)
8
- end
5
+ extend ActiveSupport::Concern
9
6
 
10
7
  module ClassMethods
11
8
 
@@ -34,7 +31,7 @@ module CouchRest
34
31
  "views" => {
35
32
  'all' => {
36
33
  'map' => "function(doc) {
37
- if (doc['couchrest-type'] == '#{self.to_s}') {
34
+ if (doc['#{self.model_type_key}'] == '#{self.to_s}') {
38
35
  emit(doc['_id'],1);
39
36
  }
40
37
  }"
@@ -8,21 +8,21 @@ module CouchRest
8
8
 
9
9
  module ClassMethods
10
10
 
11
- # Load all documents that have the "couchrest-type" field equal to the
11
+ # Load all documents that have the model_type_key's field equal to the
12
12
  # name of the current class. Take the standard set of
13
13
  # CouchRest::Database#view options.
14
14
  def all(opts = {}, &block)
15
15
  view(:all, opts, &block)
16
16
  end
17
17
 
18
- # Returns the number of documents that have the "couchrest-type" field
18
+ # Returns the number of documents that have the model_type_key's field
19
19
  # equal to the name of the current class. Takes the standard set of
20
20
  # CouchRest::Database#view options
21
21
  def count(opts = {}, &block)
22
22
  all({:raw => true, :limit => 0}.merge(opts), &block)['total_rows']
23
23
  end
24
24
 
25
- # Load the first document that have the "couchrest-type" field equal to
25
+ # Load the first document that have the model_type_key's field equal to
26
26
  # the name of the current class.
27
27
  #
28
28
  # ==== Returns
@@ -38,6 +38,22 @@ module CouchRest
38
38
  first_instance.empty? ? nil : first_instance.first
39
39
  end
40
40
 
41
+ # Load the last document that have the model_type_key's field equal to
42
+ # the name of the current class.
43
+ # It's similar to method first, just adds :descending => true
44
+ #
45
+ # ==== Returns
46
+ # Object:: The last object instance available
47
+ # or
48
+ # Nil:: if no instances available
49
+ #
50
+ # ==== Parameters
51
+ # opts<Hash>::
52
+ # View options, see <tt>CouchRest::Database#view</tt> options for more info.
53
+ def last(opts = {})
54
+ first(opts.merge!(:descending => true))
55
+ end
56
+
41
57
  # Load a document from the database by id
42
58
  # No exceptions will be raised if the document isn't found
43
59
  #
@@ -70,6 +86,7 @@ module CouchRest
70
86
  # id<String, Integer>:: Document ID
71
87
  # db<Database>:: optional option to pass a custom database to use
72
88
  def get!(id, db = database)
89
+ raise "Missing or empty document ID" if id.to_s.empty?
73
90
  doc = db.get id
74
91
  create_from_database(doc)
75
92
  end
@@ -62,6 +62,20 @@ module CouchRest
62
62
  end
63
63
  end
64
64
 
65
+ # Update the document's attributes and save. For example:
66
+ #
67
+ # doc.update_attributes :name => "Fred"
68
+ #
69
+ # Is the equivilent of doing the following:
70
+ #
71
+ # doc.attributes = { :name => "Fred" }
72
+ # doc.save
73
+ #
74
+ def update_attributes(hash)
75
+ update_attributes_without_saving hash
76
+ save
77
+ end
78
+
65
79
  protected
66
80
 
67
81
  def perform_validations(options = {})
@@ -83,7 +97,7 @@ module CouchRest
83
97
  # ==== Returns
84
98
  # a document instance
85
99
  def create_from_database(doc = {})
86
- base = (doc['couchrest-type'].blank? || doc['couchrest-type'] == self.to_s) ? self : doc['couchrest-type'].constantize
100
+ base = (doc[model_type_key].blank? || doc[model_type_key] == self.to_s) ? self : doc[model_type_key].constantize
87
101
  base.new(doc, :directly_set_attributes => true)
88
102
  end
89
103
 
@@ -2,16 +2,12 @@
2
2
  module CouchRest
3
3
  module Model
4
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?(:[]=))
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ extlib_inheritable_accessor(:properties) unless self.respond_to?(:properties)
9
+ self.properties ||= []
10
+ raise "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 (method_defined?(:[]) && method_defined?(:[]=))
15
11
  end
16
12
 
17
13
  # Returns the Class properties
@@ -22,16 +18,45 @@ module CouchRest
22
18
  self.class.properties
23
19
  end
24
20
 
21
+ # Returns the Class properties with their values
22
+ #
23
+ # ==== Returns
24
+ # Array:: the list of properties with their values
25
+ def properties_with_values
26
+ props = {}
27
+ properties.each { |property| props[property.name] = read_attribute(property.name) }
28
+ props
29
+ end
30
+
31
+ # Read the casted value of an attribute defined with a property.
32
+ #
33
+ # ==== Returns
34
+ # Object:: the casted attibutes value.
25
35
  def read_attribute(property)
26
- self[property.to_s]
36
+ self[find_property!(property).to_s]
27
37
  end
28
38
 
39
+ # Store a casted value in the current instance of an attribute defined
40
+ # with a property.
29
41
  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)
42
+ prop = find_property!(property)
43
+ self[prop.to_s] = prop.is_a?(String) ? value : prop.cast(self, value)
44
+ end
45
+
46
+ # Takes a hash as argument, and applies the values by using writer methods
47
+ # for each key. It doesn't save the document at the end. Raises a NoMethodError if the corresponding methods are
48
+ # missing. In case of error, no attributes are changed.
49
+ def update_attributes_without_saving(hash)
50
+ # Remove any protected and update all the rest. Any attributes
51
+ # which do not have a property will simply be ignored.
52
+ attrs = remove_protected_attributes(hash)
53
+ directly_set_attributes(attrs)
33
54
  end
55
+ alias :attributes= :update_attributes_without_saving
34
56
 
57
+
58
+ private
59
+ # The following methods should be accessable by the Model::Base Class, but not by anything else!
35
60
  def apply_all_property_defaults
36
61
  return if self.respond_to?(:new?) && (new? == false)
37
62
  # TODO: cache the default object
@@ -39,9 +64,57 @@ module CouchRest
39
64
  write_attribute(property, property.default_value)
40
65
  end
41
66
  end
42
-
67
+
68
+ def prepare_all_attributes(doc = {}, options = {})
69
+ apply_all_property_defaults
70
+ if options[:directly_set_attributes]
71
+ directly_set_read_only_attributes(doc)
72
+ else
73
+ doc = remove_protected_attributes(doc)
74
+ end
75
+ directly_set_attributes(doc) unless doc.nil?
76
+ end
77
+
78
+ def find_property!(property)
79
+ prop = property.is_a?(Property) ? property : self.class.properties.detect {|p| p.to_s == property.to_s}
80
+ raise ArgumentError, "Missing property definition for #{property.to_s}" if prop.nil?
81
+ prop
82
+ end
83
+
84
+ # Set all the attributes and return a hash with the attributes
85
+ # that have not been accepted.
86
+ def directly_set_attributes(hash)
87
+ hash.reject do |attribute_name, attribute_value|
88
+ if self.respond_to?("#{attribute_name}=")
89
+ self.send("#{attribute_name}=", attribute_value)
90
+ true
91
+ elsif mass_assign_any_attribute # config option
92
+ self[attribute_name] = attribute_value
93
+ true
94
+ else
95
+ false
96
+ end
97
+ end
98
+ end
99
+
100
+ def directly_set_read_only_attributes(hash)
101
+ property_list = self.properties.map{|p| p.name}
102
+ hash.each do |attribute_name, attribute_value|
103
+ next if self.respond_to?("#{attribute_name}=")
104
+ if property_list.include?(attribute_name)
105
+ write_attribute(attribute_name, hash.delete(attribute_name))
106
+ end
107
+ end
108
+ end
109
+
110
+ def set_attributes(hash)
111
+ attrs = remove_protected_attributes(hash)
112
+ directly_set_attributes(attrs)
113
+ end
114
+
115
+
43
116
  module ClassMethods
44
-
117
+
45
118
  def property(name, *options, &block)
46
119
  opts = { }
47
120
  type = options.shift
@@ -64,16 +137,16 @@ module CouchRest
64
137
  class_eval <<-EOS, __FILE__, __LINE__
65
138
  property(:updated_at, Time, :read_only => true, :protected => true, :auto_validation => false)
66
139
  property(:created_at, Time, :read_only => true, :protected => true, :auto_validation => false)
67
-
140
+
68
141
  set_callback :save, :before do |object|
69
142
  write_attribute('updated_at', Time.now)
70
143
  write_attribute('created_at', Time.now) if object.new?
71
144
  end
72
145
  EOS
73
146
  end
74
-
147
+
75
148
  protected
76
-
149
+
77
150
  # This is not a thread safe operation, if you have to set new properties at runtime
78
151
  # make sure a mutex is used.
79
152
  def define_property(name, options={}, &block)
@@ -87,7 +160,7 @@ module CouchRest
87
160
  type = [type] # inject as an array
88
161
  end
89
162
  property = Property.new(name, type, options)
90
- create_property_getter(property)
163
+ create_property_getter(property)
91
164
  create_property_setter(property) unless property.read_only == true
92
165
  if property.type_class.respond_to?(:validates_casted_model)
93
166
  validates_casted_model property.name
@@ -95,7 +168,7 @@ module CouchRest
95
168
  properties << property
96
169
  property
97
170
  end
98
-
171
+
99
172
  # defines the getter for the property (and optional aliases)
100
173
  def create_property_getter(property)
101
174
  # meth = property.name
@@ -136,9 +209,10 @@ module CouchRest
136
209
  EOS
137
210
  end
138
211
  end
139
-
212
+
140
213
  end # module ClassMethods
141
-
214
+
142
215
  end
143
216
  end
144
217
  end
218
+
@@ -33,7 +33,7 @@ module CouchRest::Model
33
33
  data.keys.sort.each do |k|
34
34
  value << data[k]
35
35
  end
36
- elsif value.class != Array
36
+ elsif !value.is_a?(Array)
37
37
  raise "Expecting an array or keyed hash for property #{parent.class.name}##{self.name}"
38
38
  end
39
39
  arr = value.collect { |data| cast_value(parent, data) }
@@ -58,7 +58,8 @@ module CouchRest::Model
58
58
  if default.class == Proc
59
59
  default.call
60
60
  else
61
- Marshal.load(Marshal.dump(default))
61
+ # Marshal.load(Marshal.dump(default)) # Removed as there are no failing tests and caused mutex errors
62
+ default
62
63
  end
63
64
  end
64
65
 
@@ -72,12 +73,12 @@ module CouchRest::Model
72
73
  def parse_type(type)
73
74
  if type.nil?
74
75
  @casted = false
75
- @type = nil
76
+ @type = nil
76
77
  @type_class = nil
77
78
  else
78
79
  base = type.is_a?(Array) ? type.first : type
79
80
  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
+ raise "Defining a property type as a #{type.class.name.humanize} is not supported in CouchRest Model!" if base.class != Class
81
82
  @type_class = base
82
83
  @type = type
83
84
  end
@@ -0,0 +1,71 @@
1
+ module CouchRest
2
+ module Model
3
+ module PropertyProtection
4
+ extend ActiveSupport::Concern
5
+
6
+ # Property protection from mass assignment to CouchRest::Model properties
7
+ #
8
+ # Protected methods will be removed from
9
+ # * new
10
+ # * update_attributes
11
+ # * upate_attributes_without_saving
12
+ # * attributes=
13
+ #
14
+ # There are two modes of protection
15
+ # 1) Declare accessible poperties, and assume all unspecified properties are protected
16
+ # property :name, :accessible => true
17
+ # property :admin # this will be automatically protected
18
+ #
19
+ # 2) Declare protected properties, and assume all unspecified properties are accessible
20
+ # property :name # this will not be protected
21
+ # property :admin, :protected => true
22
+ #
23
+ # 3) Mix and match, and assume all unspecified properties are protected.
24
+ # property :name, :accessible => true
25
+ # property :admin, :protected => true # ignored
26
+ # property :phone # this will be automatically protected
27
+ #
28
+ # Note: the timestamps! method protectes the created_at and updated_at properties
29
+
30
+
31
+ def self.included(base)
32
+ base.extend(ClassMethods)
33
+ end
34
+
35
+ module ClassMethods
36
+ def accessible_properties
37
+ props = properties.select { |prop| prop.options[:accessible] }
38
+ if props.empty?
39
+ props = properties.select { |prop| !prop.options[:protected] }
40
+ end
41
+ props
42
+ end
43
+
44
+ def protected_properties
45
+ accessibles = accessible_properties
46
+ properties.reject { |prop| accessibles.include?(prop) }
47
+ end
48
+ end
49
+
50
+ def accessible_properties
51
+ self.class.accessible_properties
52
+ end
53
+
54
+ def protected_properties
55
+ self.class.protected_properties
56
+ end
57
+
58
+ # Return a new copy of the attributes hash with protected attributes
59
+ # removed.
60
+ def remove_protected_attributes(attributes)
61
+ protected_names = protected_properties.map { |prop| prop.name }
62
+ return attributes if protected_names.empty? or attributes.nil?
63
+
64
+ attributes.reject do |property_name, property_value|
65
+ protected_names.include?(property_name.to_s)
66
+ end
67
+ end
68
+
69
+ end
70
+ end
71
+ end
@@ -1,19 +1,23 @@
1
1
  class Time
2
2
  # returns a local time value much faster than Time.parse
3
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})/
4
+ string =~ /(\d{4})[\-|\/](\d{2})[\-|\/](\d{2})[T|\s](\d{2}):(\d{2}):(\d{2})(([\+|\s|\-])*(\d{2}):?(\d{2}))?/
5
5
  # $1 = year
6
6
  # $2 = month
7
7
  # $3 = day
8
8
  # $4 = hours
9
9
  # $5 = minutes
10
10
  # $6 = seconds
11
- # $7 = time zone direction
12
- # $8 = tz difference
11
+ # $8 = time zone direction
12
+ # $9 = tz difference
13
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)
14
+ time = mktime($1, RFC2822_MONTH_NAME[$2.to_i - 1], $3, $4, $5, $6)
15
+ if ($7)
16
+ tz_difference = ("#{$8 == '-' ? '+' : '-'}#{$9}".to_i * 3600)
17
+ time + tz_difference + zone_offset(time.zone)
18
+ else
19
+ time
20
+ end
17
21
  end
18
22
  end
19
23
 
@@ -75,7 +79,7 @@ module CouchRest
75
79
  # Match numeric string
76
80
  def typecast_to_numeric(value, method)
77
81
  if value.respond_to?(:to_str)
78
- if value.to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
82
+ if value.gsub(/,/, '.').gsub(/\.(?!\d*\Z)/, '').to_str =~ /\A(-?(?:0|[1-9]\d*)(?:\.\d+)?|(?:\.\d+))\z/
79
83
  $1.send(method)
80
84
  else
81
85
  value
@@ -128,6 +132,7 @@ module CouchRest
128
132
  rescue ArgumentError
129
133
  value
130
134
  rescue TypeError
135
+ # After failures, resort to normal time parse
131
136
  value
132
137
  end
133
138