activemodel 4.2.0 → 6.1.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. checksums.yaml +5 -5
  2. data/CHANGELOG.md +49 -37
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +16 -22
  5. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute_assignment.rb +55 -0
  8. data/lib/active_model/attribute_methods.rb +150 -73
  9. data/lib/active_model/attribute_mutation_tracker.rb +181 -0
  10. data/lib/active_model/attribute_set/builder.rb +191 -0
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  12. data/lib/active_model/attribute_set.rb +106 -0
  13. data/lib/active_model/attributes.rb +132 -0
  14. data/lib/active_model/callbacks.rb +31 -25
  15. data/lib/active_model/conversion.rb +20 -9
  16. data/lib/active_model/dirty.rb +142 -116
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +436 -202
  19. data/lib/active_model/forbidden_attributes_protection.rb +6 -3
  20. data/lib/active_model/gem_version.rb +5 -3
  21. data/lib/active_model/lint.rb +47 -42
  22. data/lib/active_model/locale/en.yml +2 -1
  23. data/lib/active_model/model.rb +7 -7
  24. data/lib/active_model/naming.rb +36 -18
  25. data/lib/active_model/nested_error.rb +22 -0
  26. data/lib/active_model/railtie.rb +8 -0
  27. data/lib/active_model/secure_password.rb +61 -67
  28. data/lib/active_model/serialization.rb +48 -17
  29. data/lib/active_model/serializers/json.rb +22 -13
  30. data/lib/active_model/translation.rb +5 -4
  31. data/lib/active_model/type/big_integer.rb +14 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +46 -0
  34. data/lib/active_model/type/date.rb +52 -0
  35. data/lib/active_model/type/date_time.rb +46 -0
  36. data/lib/active_model/type/decimal.rb +69 -0
  37. data/lib/active_model/type/float.rb +35 -0
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +49 -0
  39. data/lib/active_model/type/helpers/mutable.rb +20 -0
  40. data/lib/active_model/type/helpers/numeric.rb +48 -0
  41. data/lib/active_model/type/helpers/time_value.rb +90 -0
  42. data/lib/active_model/type/helpers/timezone.rb +19 -0
  43. data/lib/active_model/type/helpers.rb +7 -0
  44. data/lib/active_model/type/immutable_string.rb +35 -0
  45. data/lib/active_model/type/integer.rb +67 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +35 -0
  48. data/lib/active_model/type/time.rb +46 -0
  49. data/lib/active_model/type/value.rb +133 -0
  50. data/lib/active_model/type.rb +53 -0
  51. data/lib/active_model/validations/absence.rb +6 -4
  52. data/lib/active_model/validations/acceptance.rb +72 -14
  53. data/lib/active_model/validations/callbacks.rb +23 -19
  54. data/lib/active_model/validations/clusivity.rb +18 -12
  55. data/lib/active_model/validations/confirmation.rb +27 -14
  56. data/lib/active_model/validations/exclusion.rb +7 -4
  57. data/lib/active_model/validations/format.rb +27 -27
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +8 -7
  60. data/lib/active_model/validations/length.rb +35 -32
  61. data/lib/active_model/validations/numericality.rb +72 -34
  62. data/lib/active_model/validations/presence.rb +3 -3
  63. data/lib/active_model/validations/validates.rb +17 -15
  64. data/lib/active_model/validations/with.rb +6 -12
  65. data/lib/active_model/validations.rb +58 -23
  66. data/lib/active_model/validator.rb +23 -17
  67. data/lib/active_model/version.rb +4 -2
  68. data/lib/active_model.rb +18 -11
  69. metadata +44 -25
  70. data/lib/active_model/serializers/xml.rb +0 -238
  71. data/lib/active_model/test_case.rb +0 -4
@@ -1,5 +1,6 @@
1
- require 'active_support/core_ext/hash/except'
2
- require 'active_support/core_ext/hash/slice'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/enumerable"
3
4
 
4
5
  module ActiveModel
5
6
  # == Active \Model \Serialization
@@ -31,16 +32,14 @@ module ActiveModel
31
32
  # of the attributes hash's keys. In order to override this behavior, take a look
32
33
  # at the private method +read_attribute_for_serialization+.
33
34
  #
34
- # Most of the time though, either the JSON or XML serializations are needed.
35
- # Both of these modules automatically include the
36
- # <tt>ActiveModel::Serialization</tt> module, so there is no need to
37
- # explicitly include it.
35
+ # ActiveModel::Serializers::JSON module automatically includes
36
+ # the <tt>ActiveModel::Serialization</tt> module, so there is no need to
37
+ # explicitly include <tt>ActiveModel::Serialization</tt>.
38
38
  #
39
- # A minimal implementation including XML and JSON would be:
39
+ # A minimal implementation including JSON would be:
40
40
  #
41
41
  # class Person
42
42
  # include ActiveModel::Serializers::JSON
43
- # include ActiveModel::Serializers::Xml
44
43
  #
45
44
  # attr_accessor :name
46
45
  #
@@ -55,13 +54,11 @@ module ActiveModel
55
54
  # person.serializable_hash # => {"name"=>nil}
56
55
  # person.as_json # => {"name"=>nil}
57
56
  # person.to_json # => "{\"name\":null}"
58
- # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
59
57
  #
60
58
  # person.name = "Bob"
61
59
  # person.serializable_hash # => {"name"=>"Bob"}
62
60
  # person.as_json # => {"name"=>"Bob"}
63
61
  # person.to_json # => "{\"name\":\"Bob\"}"
64
- # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
65
62
  #
66
63
  # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
67
64
  # <tt>:include</tt>. The following are all valid examples:
@@ -94,20 +91,51 @@ module ActiveModel
94
91
  # person.serializable_hash(except: :name) # => {"age"=>22}
95
92
  # person.serializable_hash(methods: :capitalized_name)
96
93
  # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
94
+ #
95
+ # Example with <tt>:include</tt> option
96
+ #
97
+ # class User
98
+ # include ActiveModel::Serializers::JSON
99
+ # attr_accessor :name, :notes # Emulate has_many :notes
100
+ # def attributes
101
+ # {'name' => nil}
102
+ # end
103
+ # end
104
+ #
105
+ # class Note
106
+ # include ActiveModel::Serializers::JSON
107
+ # attr_accessor :title, :text
108
+ # def attributes
109
+ # {'title' => nil, 'text' => nil}
110
+ # end
111
+ # end
112
+ #
113
+ # note = Note.new
114
+ # note.title = 'Battle of Austerlitz'
115
+ # note.text = 'Some text here'
116
+ #
117
+ # user = User.new
118
+ # user.name = 'Napoleon'
119
+ # user.notes = [note]
120
+ #
121
+ # user.serializable_hash
122
+ # # => {"name" => "Napoleon"}
123
+ # user.serializable_hash(include: { notes: { only: 'title' }})
124
+ # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
97
125
  def serializable_hash(options = nil)
98
- options ||= {}
99
-
100
126
  attribute_names = attributes.keys
127
+
128
+ return serializable_attributes(attribute_names) if options.blank?
129
+
101
130
  if only = options[:only]
102
131
  attribute_names &= Array(only).map(&:to_s)
103
132
  elsif except = options[:except]
104
133
  attribute_names -= Array(except).map(&:to_s)
105
134
  end
106
135
 
107
- hash = {}
108
- attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
136
+ hash = serializable_attributes(attribute_names)
109
137
 
110
- Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) }
138
+ Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
111
139
 
112
140
  serializable_add_includes(options) do |association, records, opts|
113
141
  hash[association.to_s] = if records.respond_to?(:to_ary)
@@ -121,7 +149,6 @@ module ActiveModel
121
149
  end
122
150
 
123
151
  private
124
-
125
152
  # Hook method defining how an attribute value should be retrieved for
126
153
  # serialization. By default this is assumed to be an instance named after
127
154
  # the attribute. Override this method in subclasses should you need to
@@ -140,6 +167,10 @@ module ActiveModel
140
167
  # end
141
168
  alias :read_attribute_for_serialization :send
142
169
 
170
+ def serializable_attributes(attribute_names)
171
+ attribute_names.index_with { |n| read_attribute_for_serialization(n) }
172
+ end
173
+
143
174
  # Add associations specified via the <tt>:include</tt> option.
144
175
  #
145
176
  # Expects a block that takes as arguments:
@@ -150,7 +181,7 @@ module ActiveModel
150
181
  return unless includes = options[:include]
151
182
 
152
183
  unless includes.is_a?(Hash)
153
- includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
184
+ includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
154
185
  end
155
186
 
156
187
  includes.each do |association, opts|
@@ -1,4 +1,6 @@
1
- require 'active_support/json'
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/json"
2
4
 
3
5
  module ActiveModel
4
6
  module Serializers
@@ -10,8 +12,7 @@ module ActiveModel
10
12
  included do
11
13
  extend ActiveModel::Naming
12
14
 
13
- class_attribute :include_root_in_json
14
- self.include_root_in_json = false
15
+ class_attribute :include_root_in_json, instance_writer: false, default: false
15
16
  end
16
17
 
17
18
  # Returns a hash representing the model. Some configuration can be
@@ -25,13 +26,13 @@ module ActiveModel
25
26
  # user = User.find(1)
26
27
  # user.as_json
27
28
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
28
- # # "created_at" => "2006/08/01", "awesome" => true}
29
+ # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
29
30
  #
30
31
  # ActiveRecord::Base.include_root_in_json = true
31
32
  #
32
33
  # user.as_json
33
34
  # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
34
- # # "created_at" => "2006/08/01", "awesome" => true } }
35
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
35
36
  #
36
37
  # This behavior can also be achieved by setting the <tt>:root</tt> option
37
38
  # to +true+ as in:
@@ -39,7 +40,14 @@ module ActiveModel
39
40
  # user = User.find(1)
40
41
  # user.as_json(root: true)
41
42
  # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
42
- # # "created_at" => "2006/08/01", "awesome" => true } }
43
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
44
+ #
45
+ # If you prefer, <tt>:root</tt> may also be set to a custom string key instead as in:
46
+ #
47
+ # user = User.find(1)
48
+ # user.as_json(root: "author")
49
+ # # => { "author" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
50
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
43
51
  #
44
52
  # Without any +options+, the returned Hash will include all the model's
45
53
  # attributes.
@@ -47,7 +55,7 @@ module ActiveModel
47
55
  # user = User.find(1)
48
56
  # user.as_json
49
57
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
50
- # # "created_at" => "2006/08/01", "awesome" => true}
58
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
51
59
  #
52
60
  # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
53
61
  # the attributes included, and work similar to the +attributes+ method.
@@ -62,14 +70,14 @@ module ActiveModel
62
70
  #
63
71
  # user.as_json(methods: :permalink)
64
72
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
65
- # # "created_at" => "2006/08/01", "awesome" => true,
73
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
66
74
  # # "permalink" => "1-konata-izumi" }
67
75
  #
68
76
  # To include associations use <tt>:include</tt>:
69
77
  #
70
78
  # user.as_json(include: :posts)
71
79
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
72
- # # "created_at" => "2006/08/01", "awesome" => true,
80
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
73
81
  # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
74
82
  # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
75
83
  #
@@ -80,7 +88,7 @@ module ActiveModel
80
88
  # only: :body } },
81
89
  # only: :title } })
82
90
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
83
- # # "created_at" => "2006/08/01", "awesome" => true,
91
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
84
92
  # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
85
93
  # # "title" => "Welcome to the weblog" },
86
94
  # # { "comments" => [ { "body" => "Don't think too hard" } ],
@@ -92,11 +100,12 @@ module ActiveModel
92
100
  include_root_in_json
93
101
  end
94
102
 
103
+ hash = serializable_hash(options).as_json
95
104
  if root
96
105
  root = model_name.element if root == true
97
- { root => serializable_hash(options) }
106
+ { root => hash }
98
107
  else
99
- serializable_hash(options)
108
+ hash
100
109
  end
101
110
  end
102
111
 
@@ -134,7 +143,7 @@ module ActiveModel
134
143
  # person.name # => "bob"
135
144
  # person.age # => 22
136
145
  # person.awesome # => true
137
- def from_json(json, include_root=include_root_in_json)
146
+ def from_json(json, include_root = include_root_in_json)
138
147
  hash = ActiveSupport::JSON.decode(json)
139
148
  hash = hash.values.first if include_root
140
149
  self.attributes = hash
@@ -1,5 +1,6 @@
1
- module ActiveModel
1
+ # frozen_string_literal: true
2
2
 
3
+ module ActiveModel
3
4
  # == Active \Model \Translation
4
5
  #
5
6
  # Provides integration between your object and the Rails internationalization
@@ -31,7 +32,7 @@ module ActiveModel
31
32
  # ActiveModel::Errors#full_messages and
32
33
  # ActiveModel::Translation#human_attribute_name.
33
34
  def lookup_ancestors
34
- self.ancestors.select { |x| x.respond_to?(:model_name) }
35
+ ancestors.select { |x| x.respond_to?(:model_name) }
35
36
  end
36
37
 
37
38
  # Transforms attribute names into a more human format, such as "First name"
@@ -45,7 +46,7 @@ module ActiveModel
45
46
  parts = attribute.to_s.split(".")
46
47
  attribute = parts.pop
47
48
  namespace = parts.join("/") unless parts.empty?
48
- attributes_scope = "#{self.i18n_scope}.attributes"
49
+ attributes_scope = "#{i18n_scope}.attributes"
49
50
 
50
51
  if namespace
51
52
  defaults = lookup_ancestors.map do |klass|
@@ -63,7 +64,7 @@ module ActiveModel
63
64
  defaults << attribute.humanize
64
65
 
65
66
  options[:default] = defaults
66
- I18n.translate(defaults.shift, options)
67
+ I18n.translate(defaults.shift, **options)
67
68
  end
68
69
  end
69
70
  end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/type/integer"
4
+
5
+ module ActiveModel
6
+ module Type
7
+ class BigInteger < Integer # :nodoc:
8
+ private
9
+ def max_value
10
+ ::Float::INFINITY
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ class Binary < Value # :nodoc:
6
+ def type
7
+ :binary
8
+ end
9
+
10
+ def binary?
11
+ true
12
+ end
13
+
14
+ def cast(value)
15
+ if value.is_a?(Data)
16
+ value.to_s
17
+ else
18
+ super
19
+ end
20
+ end
21
+
22
+ def serialize(value)
23
+ return if value.nil?
24
+ Data.new(super)
25
+ end
26
+
27
+ def changed_in_place?(raw_old_value, value)
28
+ old_value = deserialize(raw_old_value)
29
+ old_value != value
30
+ end
31
+
32
+ class Data # :nodoc:
33
+ def initialize(value)
34
+ @value = value.to_s
35
+ end
36
+
37
+ def to_s
38
+ @value
39
+ end
40
+ alias_method :to_str, :to_s
41
+
42
+ def hex
43
+ @value.unpack1("H*")
44
+ end
45
+
46
+ def ==(other)
47
+ other == to_s || super
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ # == Active \Model \Type \Boolean
6
+ #
7
+ # A class that behaves like a boolean type, including rules for coercion of user input.
8
+ #
9
+ # === Coercion
10
+ # Values set from user input will first be coerced into the appropriate ruby type.
11
+ # Coercion behavior is roughly mapped to Ruby's boolean semantics.
12
+ #
13
+ # - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+
14
+ # - Empty strings are coerced to +nil+
15
+ # - All other values will be coerced to +true+
16
+ class Boolean < Value
17
+ FALSE_VALUES = [
18
+ false, 0,
19
+ "0", :"0",
20
+ "f", :f,
21
+ "F", :F,
22
+ "false", :false,
23
+ "FALSE", :FALSE,
24
+ "off", :off,
25
+ "OFF", :OFF,
26
+ ].to_set.freeze
27
+
28
+ def type # :nodoc:
29
+ :boolean
30
+ end
31
+
32
+ def serialize(value) # :nodoc:
33
+ cast(value)
34
+ end
35
+
36
+ private
37
+ def cast_value(value)
38
+ if value == ""
39
+ nil
40
+ else
41
+ !FALSE_VALUES.include?(value)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ class Date < Value # :nodoc:
6
+ include Helpers::Timezone
7
+ include Helpers::AcceptsMultiparameterTime.new
8
+
9
+ def type
10
+ :date
11
+ end
12
+
13
+ def type_cast_for_schema(value)
14
+ value.to_s(:db).inspect
15
+ end
16
+
17
+ private
18
+ def cast_value(value)
19
+ if value.is_a?(::String)
20
+ return if value.empty?
21
+ fast_string_to_date(value) || fallback_string_to_date(value)
22
+ elsif value.respond_to?(:to_date)
23
+ value.to_date
24
+ else
25
+ value
26
+ end
27
+ end
28
+
29
+ ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
30
+ def fast_string_to_date(string)
31
+ if string =~ ISO_DATE
32
+ new_date $1.to_i, $2.to_i, $3.to_i
33
+ end
34
+ end
35
+
36
+ def fallback_string_to_date(string)
37
+ new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
38
+ end
39
+
40
+ def new_date(year, mon, mday)
41
+ unless year.nil? || (year == 0 && mon == 0 && mday == 0)
42
+ ::Date.new(year, mon, mday) rescue nil
43
+ end
44
+ end
45
+
46
+ def value_from_multiparameter_assignment(*)
47
+ time = super
48
+ time && new_date(time.year, time.mon, time.mday)
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ class DateTime < Value # :nodoc:
6
+ include Helpers::Timezone
7
+ include Helpers::TimeValue
8
+ include Helpers::AcceptsMultiparameterTime.new(
9
+ defaults: { 4 => 0, 5 => 0 }
10
+ )
11
+
12
+ def type
13
+ :datetime
14
+ end
15
+
16
+ private
17
+ def cast_value(value)
18
+ return apply_seconds_precision(value) unless value.is_a?(::String)
19
+ return if value.empty?
20
+
21
+ fast_string_to_time(value) || fallback_string_to_time(value)
22
+ end
23
+
24
+ # '0.123456' -> 123456
25
+ # '1.123456' -> 123456
26
+ def microseconds(time)
27
+ time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
28
+ end
29
+
30
+ def fallback_string_to_time(string)
31
+ time_hash = ::Date._parse(string)
32
+ time_hash[:sec_fraction] = microseconds(time_hash)
33
+
34
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
35
+ end
36
+
37
+ def value_from_multiparameter_assignment(values_hash)
38
+ missing_parameters = [1, 2, 3].delete_if { |key| values_hash.key?(key) }
39
+ unless missing_parameters.empty?
40
+ raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
41
+ end
42
+ super
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal/util"
4
+
5
+ module ActiveModel
6
+ module Type
7
+ class Decimal < Value # :nodoc:
8
+ include Helpers::Numeric
9
+ BIGDECIMAL_PRECISION = 18
10
+
11
+ def type
12
+ :decimal
13
+ end
14
+
15
+ def type_cast_for_schema(value)
16
+ value.to_s.inspect
17
+ end
18
+
19
+ private
20
+ def cast_value(value)
21
+ casted_value = \
22
+ case value
23
+ when ::Float
24
+ convert_float_to_big_decimal(value)
25
+ when ::Numeric
26
+ BigDecimal(value, precision || BIGDECIMAL_PRECISION)
27
+ when ::String
28
+ begin
29
+ value.to_d
30
+ rescue ArgumentError
31
+ BigDecimal(0)
32
+ end
33
+ else
34
+ if value.respond_to?(:to_d)
35
+ value.to_d
36
+ else
37
+ cast_value(value.to_s)
38
+ end
39
+ end
40
+
41
+ apply_scale(casted_value)
42
+ end
43
+
44
+ def convert_float_to_big_decimal(value)
45
+ if precision
46
+ BigDecimal(apply_scale(value), float_precision)
47
+ else
48
+ value.to_d
49
+ end
50
+ end
51
+
52
+ def float_precision
53
+ if precision.to_i > ::Float::DIG + 1
54
+ ::Float::DIG + 1
55
+ else
56
+ precision.to_i
57
+ end
58
+ end
59
+
60
+ def apply_scale(value)
61
+ if scale
62
+ value.round(scale)
63
+ else
64
+ value
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/object/try"
4
+
5
+ module ActiveModel
6
+ module Type
7
+ class Float < Value # :nodoc:
8
+ include Helpers::Numeric
9
+
10
+ def type
11
+ :float
12
+ end
13
+
14
+ def type_cast_for_schema(value)
15
+ return "::Float::NAN" if value.try(:nan?)
16
+ case value
17
+ when ::Float::INFINITY then "::Float::INFINITY"
18
+ when -::Float::INFINITY then "-::Float::INFINITY"
19
+ else super
20
+ end
21
+ end
22
+
23
+ private
24
+ def cast_value(value)
25
+ case value
26
+ when ::Float then value
27
+ when "Infinity" then ::Float::INFINITY
28
+ when "-Infinity" then -::Float::INFINITY
29
+ when "NaN" then ::Float::NAN
30
+ else value.to_f
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module Helpers # :nodoc: all
6
+ class AcceptsMultiparameterTime < Module
7
+ module InstanceMethods
8
+ def serialize(value)
9
+ super(cast(value))
10
+ end
11
+
12
+ def cast(value)
13
+ if value.is_a?(Hash)
14
+ value_from_multiparameter_assignment(value)
15
+ else
16
+ super(value)
17
+ end
18
+ end
19
+
20
+ def assert_valid_value(value)
21
+ if value.is_a?(Hash)
22
+ value_from_multiparameter_assignment(value)
23
+ else
24
+ super(value)
25
+ end
26
+ end
27
+
28
+ def value_constructed_by_mass_assignment?(value)
29
+ value.is_a?(Hash)
30
+ end
31
+ end
32
+
33
+ def initialize(defaults: {})
34
+ include InstanceMethods
35
+
36
+ define_method(:value_from_multiparameter_assignment) do |values_hash|
37
+ defaults.each do |k, v|
38
+ values_hash[k] ||= v
39
+ end
40
+ return unless values_hash[1] && values_hash[2] && values_hash[3]
41
+ values = values_hash.sort.map!(&:last)
42
+ ::Time.public_send(default_timezone, *values)
43
+ end
44
+ private :value_from_multiparameter_assignment
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module Helpers # :nodoc: all
6
+ module Mutable
7
+ def cast(value)
8
+ deserialize(serialize(value))
9
+ end
10
+
11
+ # +raw_old_value+ will be the `_before_type_cast` version of the
12
+ # value (likely a string). +new_value+ will be the current, type
13
+ # cast value.
14
+ def changed_in_place?(raw_old_value, new_value)
15
+ raw_old_value != serialize(new_value)
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end