omg-activemodel 8.0.0.alpha1

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 (77) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +99 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +55 -0
  8. data/lib/active_model/attribute.rb +277 -0
  9. data/lib/active_model/attribute_assignment.rb +78 -0
  10. data/lib/active_model/attribute_methods.rb +592 -0
  11. data/lib/active_model/attribute_mutation_tracker.rb +189 -0
  12. data/lib/active_model/attribute_registration.rb +117 -0
  13. data/lib/active_model/attribute_set/builder.rb +182 -0
  14. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  15. data/lib/active_model/attribute_set.rb +118 -0
  16. data/lib/active_model/attributes.rb +165 -0
  17. data/lib/active_model/callbacks.rb +155 -0
  18. data/lib/active_model/conversion.rb +121 -0
  19. data/lib/active_model/deprecator.rb +7 -0
  20. data/lib/active_model/dirty.rb +416 -0
  21. data/lib/active_model/error.rb +208 -0
  22. data/lib/active_model/errors.rb +547 -0
  23. data/lib/active_model/forbidden_attributes_protection.rb +33 -0
  24. data/lib/active_model/gem_version.rb +17 -0
  25. data/lib/active_model/lint.rb +118 -0
  26. data/lib/active_model/locale/en.yml +38 -0
  27. data/lib/active_model/model.rb +78 -0
  28. data/lib/active_model/naming.rb +359 -0
  29. data/lib/active_model/nested_error.rb +22 -0
  30. data/lib/active_model/railtie.rb +24 -0
  31. data/lib/active_model/secure_password.rb +231 -0
  32. data/lib/active_model/serialization.rb +198 -0
  33. data/lib/active_model/serializers/json.rb +154 -0
  34. data/lib/active_model/translation.rb +78 -0
  35. data/lib/active_model/type/big_integer.rb +36 -0
  36. data/lib/active_model/type/binary.rb +62 -0
  37. data/lib/active_model/type/boolean.rb +48 -0
  38. data/lib/active_model/type/date.rb +78 -0
  39. data/lib/active_model/type/date_time.rb +88 -0
  40. data/lib/active_model/type/decimal.rb +107 -0
  41. data/lib/active_model/type/float.rb +64 -0
  42. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
  43. data/lib/active_model/type/helpers/mutable.rb +24 -0
  44. data/lib/active_model/type/helpers/numeric.rb +61 -0
  45. data/lib/active_model/type/helpers/time_value.rb +127 -0
  46. data/lib/active_model/type/helpers/timezone.rb +23 -0
  47. data/lib/active_model/type/helpers.rb +7 -0
  48. data/lib/active_model/type/immutable_string.rb +71 -0
  49. data/lib/active_model/type/integer.rb +113 -0
  50. data/lib/active_model/type/registry.rb +37 -0
  51. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  52. data/lib/active_model/type/string.rb +43 -0
  53. data/lib/active_model/type/time.rb +87 -0
  54. data/lib/active_model/type/value.rb +157 -0
  55. data/lib/active_model/type.rb +55 -0
  56. data/lib/active_model/validations/absence.rb +33 -0
  57. data/lib/active_model/validations/acceptance.rb +113 -0
  58. data/lib/active_model/validations/callbacks.rb +119 -0
  59. data/lib/active_model/validations/clusivity.rb +54 -0
  60. data/lib/active_model/validations/comparability.rb +18 -0
  61. data/lib/active_model/validations/comparison.rb +90 -0
  62. data/lib/active_model/validations/confirmation.rb +80 -0
  63. data/lib/active_model/validations/exclusion.rb +49 -0
  64. data/lib/active_model/validations/format.rb +112 -0
  65. data/lib/active_model/validations/helper_methods.rb +15 -0
  66. data/lib/active_model/validations/inclusion.rb +47 -0
  67. data/lib/active_model/validations/length.rb +130 -0
  68. data/lib/active_model/validations/numericality.rb +222 -0
  69. data/lib/active_model/validations/presence.rb +39 -0
  70. data/lib/active_model/validations/resolve_value.rb +26 -0
  71. data/lib/active_model/validations/validates.rb +175 -0
  72. data/lib/active_model/validations/with.rb +154 -0
  73. data/lib/active_model/validations.rb +489 -0
  74. data/lib/active_model/validator.rb +190 -0
  75. data/lib/active_model/version.rb +10 -0
  76. data/lib/active_model.rb +84 -0
  77. metadata +139 -0
@@ -0,0 +1,127 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/string/zones"
4
+ require "active_support/core_ext/time/zones"
5
+
6
+ module ActiveModel
7
+ module Type
8
+ module Helpers # :nodoc: all
9
+ module TimeValue
10
+ def serialize_cast_value(value)
11
+ value = apply_seconds_precision(value)
12
+
13
+ if value.acts_like?(:time)
14
+ if is_utc?
15
+ value = value.getutc if !value.utc?
16
+ else
17
+ value = value.getlocal
18
+ end
19
+ end
20
+
21
+ value
22
+ end
23
+
24
+ def apply_seconds_precision(value)
25
+ return value unless precision && value.respond_to?(:nsec)
26
+
27
+ number_of_insignificant_digits = 9 - precision
28
+ round_power = 10**number_of_insignificant_digits
29
+ rounded_off_nsec = value.nsec % round_power
30
+
31
+ if rounded_off_nsec > 0
32
+ value.change(nsec: value.nsec - rounded_off_nsec)
33
+ else
34
+ value
35
+ end
36
+ end
37
+
38
+ def type_cast_for_schema(value)
39
+ value.to_fs(:db).inspect
40
+ end
41
+
42
+ def user_input_in_time_zone(value)
43
+ value.in_time_zone
44
+ end
45
+
46
+ private
47
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
48
+ # Treat 0000-00-00 00:00:00 as nil.
49
+ return if year.nil? || (year == 0 && mon == 0 && mday == 0)
50
+
51
+ if offset
52
+ time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
53
+ return unless time
54
+
55
+ time -= offset unless offset == 0
56
+ is_utc? ? time : time.getlocal
57
+ elsif is_utc?
58
+ ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
59
+ else
60
+ ::Time.local(year, mon, mday, hour, min, sec, microsec) rescue nil
61
+ end
62
+ end
63
+
64
+ ISO_DATETIME = /
65
+ \A
66
+ (\d{4})-(\d\d)-(\d\d)(?:T|\s) # 2020-06-20T
67
+ (\d\d):(\d\d):(\d\d)(?:\.(\d{1,6})\d*)? # 10:20:30.123456
68
+ (?:(Z(?=\z)|[+-]\d\d)(?::?(\d\d))?)? # +09:00
69
+ \z
70
+ /x
71
+
72
+ if RUBY_VERSION >= "3.2"
73
+ if Time.new(2000, 1, 1, 0, 0, 0, "-00:00").yday != 1 # Early 3.2.x had a bug
74
+ # BUG: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0
75
+ # used to return an invalid Time object
76
+ # see: https://bugs.ruby-lang.org/issues/19292
77
+ def fast_string_to_time(string)
78
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
79
+
80
+ if is_utc?
81
+ ::Time.at(::Time.new(string, in: "UTC"))
82
+ else
83
+ ::Time.new(string)
84
+ end
85
+ rescue ArgumentError
86
+ nil
87
+ end
88
+ else
89
+ def fast_string_to_time(string)
90
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
91
+
92
+ if is_utc?
93
+ ::Time.new(string, in: "UTC")
94
+ else
95
+ ::Time.new(string)
96
+ end
97
+ rescue ArgumentError
98
+ nil
99
+ end
100
+ end
101
+ else
102
+ def fast_string_to_time(string)
103
+ return unless ISO_DATETIME =~ string
104
+
105
+ usec = $7.to_i
106
+ usec_len = $7&.length
107
+ if usec_len&.< 6
108
+ usec *= 10**(6 - usec_len)
109
+ end
110
+
111
+ if $8
112
+ offset = \
113
+ if $8 == "Z"
114
+ 0
115
+ else
116
+ offset_h, offset_m = $8.to_i, $9.to_i
117
+ offset_h.to_i * 3600 + (offset_h.negative? ? -1 : 1) * offset_m * 60
118
+ end
119
+ end
120
+
121
+ new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
122
+ end
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/time/zones"
4
+
5
+ module ActiveModel
6
+ module Type
7
+ module Helpers # :nodoc: all
8
+ module Timezone
9
+ def is_utc?
10
+ if default = ::Time.zone_default
11
+ default.name == "UTC"
12
+ else
13
+ true
14
+ end
15
+ end
16
+
17
+ def default_timezone
18
+ is_utc? ? :utc : :local
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/type/helpers/accepts_multiparameter_time"
4
+ require "active_model/type/helpers/numeric"
5
+ require "active_model/type/helpers/mutable"
6
+ require "active_model/type/helpers/time_value"
7
+ require "active_model/type/helpers/timezone"
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ # = Active Model \ImmutableString \Type
6
+ #
7
+ # Attribute type to represent immutable strings. It casts incoming values to
8
+ # frozen strings.
9
+ #
10
+ # class Person
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :name, :immutable_string
14
+ # end
15
+ #
16
+ # person = Person.new
17
+ # person.name = 1
18
+ #
19
+ # person.name # => "1"
20
+ # person.name.frozen? # => true
21
+ #
22
+ # Values are coerced to strings using their +to_s+ method. Boolean values
23
+ # are treated differently, however: +true+ will be cast to <tt>"t"</tt> and
24
+ # +false+ will be cast to <tt>"f"</tt>. These strings can be customized when
25
+ # declaring an attribute:
26
+ #
27
+ # class Person
28
+ # include ActiveModel::Attributes
29
+ #
30
+ # attribute :active, :immutable_string, true: "aye", false: "nay"
31
+ # end
32
+ #
33
+ # person = Person.new
34
+ # person.active = true
35
+ #
36
+ # person.active # => "aye"
37
+ class ImmutableString < Value
38
+ def initialize(**args)
39
+ @true = -(args.delete(:true)&.to_s || "t")
40
+ @false = -(args.delete(:false)&.to_s || "f")
41
+ super
42
+ end
43
+
44
+ def type
45
+ :string
46
+ end
47
+
48
+ def serialize(value)
49
+ case value
50
+ when ::Numeric, ::Symbol, ActiveSupport::Duration then value.to_s
51
+ when true then @true
52
+ when false then @false
53
+ else super
54
+ end
55
+ end
56
+
57
+ def serialize_cast_value(value) # :nodoc:
58
+ value
59
+ end
60
+
61
+ private
62
+ def cast_value(value)
63
+ case value
64
+ when true then @true
65
+ when false then @false
66
+ else value.to_s.freeze
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ # = Active Model \Integer \Type
6
+ #
7
+ # Attribute type for integer representation. This type is registered under
8
+ # the +:integer+ key.
9
+ #
10
+ # class Person
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :age, :integer
14
+ # end
15
+ #
16
+ # Values are cast using their +to_i+ method, except for blank strings, which
17
+ # are cast to +nil+. If a +to_i+ method is not defined or raises an error,
18
+ # the value will be cast to +nil+.
19
+ #
20
+ # person = Person.new
21
+ #
22
+ # person.age = "18"
23
+ # person.age # => 18
24
+ #
25
+ # person.age = ""
26
+ # person.age # => nil
27
+ #
28
+ # person.age = :not_an_integer
29
+ # person.age # => nil (because Symbol does not define #to_i)
30
+ #
31
+ # Serialization also works under the same principle. Non-numeric strings are
32
+ # serialized as +nil+, for example.
33
+ #
34
+ # Serialization also validates that the integer can be stored using a
35
+ # limited number of bytes. If it cannot, an ActiveModel::RangeError will be
36
+ # raised. The default limit is 4 bytes, and can be customized when declaring
37
+ # an attribute:
38
+ #
39
+ # class Person
40
+ # include ActiveModel::Attributes
41
+ #
42
+ # attribute :age, :integer, limit: 6
43
+ # end
44
+ class Integer < Value
45
+ include Helpers::Numeric
46
+
47
+ # Column storage size in bytes.
48
+ # 4 bytes means an integer as opposed to smallint etc.
49
+ DEFAULT_LIMIT = 4
50
+
51
+ def initialize(**)
52
+ super
53
+ @range = min_value...max_value
54
+ end
55
+
56
+ def type
57
+ :integer
58
+ end
59
+
60
+ def deserialize(value)
61
+ return if value.blank?
62
+ value.to_i
63
+ end
64
+
65
+ def serialize(value)
66
+ return if value.is_a?(::String) && non_numeric_string?(value)
67
+ ensure_in_range(super)
68
+ end
69
+
70
+ def serialize_cast_value(value) # :nodoc:
71
+ ensure_in_range(value)
72
+ end
73
+
74
+ def serializable?(value)
75
+ cast_value = cast(value)
76
+ in_range?(cast_value) || begin
77
+ yield cast_value if block_given?
78
+ false
79
+ end
80
+ end
81
+
82
+ private
83
+ attr_reader :range
84
+
85
+ def in_range?(value)
86
+ !value || range.member?(value)
87
+ end
88
+
89
+ def cast_value(value)
90
+ value.to_i rescue nil
91
+ end
92
+
93
+ def ensure_in_range(value)
94
+ unless in_range?(value)
95
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
96
+ end
97
+ value
98
+ end
99
+
100
+ def max_value
101
+ 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
102
+ end
103
+
104
+ def min_value
105
+ -max_value
106
+ end
107
+
108
+ def _limit
109
+ limit || DEFAULT_LIMIT
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ class Registry # :nodoc:
6
+ def initialize
7
+ @registrations = {}
8
+ end
9
+
10
+ def initialize_copy(other)
11
+ @registrations = @registrations.dup
12
+ super
13
+ end
14
+
15
+ def register(type_name, klass = nil, &block)
16
+ unless block_given?
17
+ block = proc { |_, *args| klass.new(*args) }
18
+ block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
19
+ end
20
+ registrations[type_name] = block
21
+ end
22
+
23
+ def lookup(symbol, ...)
24
+ registration = registrations[symbol]
25
+
26
+ if registration
27
+ registration.call(symbol, ...)
28
+ else
29
+ raise ArgumentError, "Unknown type #{symbol.inspect}"
30
+ end
31
+ end
32
+
33
+ private
34
+ attr_reader :registrations
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module SerializeCastValue # :nodoc:
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def serialize_cast_value_compatible?
10
+ return @serialize_cast_value_compatible if defined?(@serialize_cast_value_compatible)
11
+ @serialize_cast_value_compatible = ancestors.index(instance_method(:serialize_cast_value).owner) <= ancestors.index(instance_method(:serialize).owner)
12
+ end
13
+ end
14
+
15
+ module DefaultImplementation
16
+ def serialize_cast_value(value)
17
+ value
18
+ end
19
+ end
20
+
21
+ def self.included(klass)
22
+ klass.include DefaultImplementation unless klass.method_defined?(:serialize_cast_value)
23
+ end
24
+
25
+ def self.serialize(type, value)
26
+ # Using `type.equal?(type.itself_if_...)` is a performant way to also
27
+ # ensure that `type` is not just a DelegateClass instance (e.g.
28
+ # ActiveRecord::Type::Serialized) unintentionally delegating
29
+ # SerializeCastValue methods.
30
+ if type.equal?((type.itself_if_serialize_cast_value_compatible rescue nil))
31
+ type.serialize_cast_value(value)
32
+ else
33
+ type.serialize(value)
34
+ end
35
+ end
36
+
37
+ def itself_if_serialize_cast_value_compatible
38
+ self if self.class.serialize_cast_value_compatible?
39
+ end
40
+
41
+ def initialize(...)
42
+ super
43
+ self.class.serialize_cast_value_compatible? # eagerly compute
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/type/immutable_string"
4
+
5
+ module ActiveModel
6
+ module Type
7
+ # = Active Model \String \Type
8
+ #
9
+ # Attribute type for strings. It is registered under the +:string+ key.
10
+ #
11
+ # This class is a specialization of ActiveModel::Type::ImmutableString. It
12
+ # performs coercion in the same way, and can be configured in the same way.
13
+ # However, it accounts for mutable strings, so dirty tracking can properly
14
+ # check if a string has changed.
15
+ class String < ImmutableString
16
+ def changed_in_place?(raw_old_value, new_value)
17
+ if new_value.is_a?(::String)
18
+ raw_old_value != new_value
19
+ end
20
+ end
21
+
22
+ def to_immutable_string
23
+ ImmutableString.new(
24
+ true: @true,
25
+ false: @false,
26
+ limit: limit,
27
+ precision: precision,
28
+ scale: scale,
29
+ )
30
+ end
31
+
32
+ private
33
+ def cast_value(value)
34
+ case value
35
+ when ::String then ::String.new(value)
36
+ when true then @true
37
+ when false then @false
38
+ else value.to_s
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ # = Active Model \Time \Type
6
+ #
7
+ # Attribute type for time of day representation. It is registered under the
8
+ # +:time+ key.
9
+ #
10
+ # class Event
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :start, :time
14
+ # end
15
+ #
16
+ # String values are parsed using the ISO 8601 datetime format, but are
17
+ # normalized to have a date of 2000-01-01 and be in the UTC time zone.
18
+ #
19
+ # event = Event.new
20
+ # event.start = "2004-10-25T01:23:45-06:00"
21
+ #
22
+ # event.start.class # => Time
23
+ # event.start # => 2000-01-01 07:23:45 UTC
24
+ #
25
+ # Partial time-only formats are also accepted.
26
+ #
27
+ # event.start = "00:01:02+03:00"
28
+ # event.start # => 1999-12-31 21:01:02 UTC
29
+ #
30
+ # The degree of sub-second precision can be customized when declaring an
31
+ # attribute:
32
+ #
33
+ # class Event
34
+ # include ActiveModel::Attributes
35
+ #
36
+ # attribute :start, :time, precision: 4
37
+ # end
38
+ class Time < Value
39
+ include Helpers::Timezone
40
+ include Helpers::AcceptsMultiparameterTime.new(
41
+ defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
42
+ )
43
+ include Helpers::TimeValue
44
+
45
+ def type
46
+ :time
47
+ end
48
+
49
+ def user_input_in_time_zone(value)
50
+ return unless value.present?
51
+
52
+ case value
53
+ when ::String
54
+ value = "2000-01-01 #{value}"
55
+ time_hash = begin
56
+ ::Date._parse(value)
57
+ rescue ArgumentError
58
+ end
59
+
60
+ return if time_hash.nil? || time_hash[:hour].nil?
61
+ when ::Time
62
+ value = value.change(year: 2000, day: 1, month: 1)
63
+ end
64
+
65
+ super(value)
66
+ end
67
+
68
+ private
69
+ def cast_value(value)
70
+ return apply_seconds_precision(value) unless value.is_a?(::String)
71
+ return if value.blank?
72
+
73
+ dummy_time_value = value.sub(/\A\d{4}-\d\d-\d\d(?:T|\s)|/, "2000-01-01 ")
74
+
75
+ fast_string_to_time(dummy_time_value) || begin
76
+ time_hash = begin
77
+ ::Date._parse(dummy_time_value)
78
+ rescue ArgumentError
79
+ end
80
+
81
+ return if time_hash.nil? || time_hash[:hour].nil?
82
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end