activemodel 4.2.11.3 → 5.0.0.beta1

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of activemodel might be problematic. Click here for more details.

Files changed (59) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +84 -93
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +8 -16
  5. data/lib/active_model.rb +3 -2
  6. data/lib/active_model/attribute_assignment.rb +52 -0
  7. data/lib/active_model/attribute_methods.rb +16 -16
  8. data/lib/active_model/callbacks.rb +3 -3
  9. data/lib/active_model/conversion.rb +3 -3
  10. data/lib/active_model/dirty.rb +34 -35
  11. data/lib/active_model/errors.rb +117 -63
  12. data/lib/active_model/forbidden_attributes_protection.rb +3 -2
  13. data/lib/active_model/gem_version.rb +5 -5
  14. data/lib/active_model/lint.rb +32 -28
  15. data/lib/active_model/locale/en.yml +2 -1
  16. data/lib/active_model/model.rb +3 -4
  17. data/lib/active_model/naming.rb +5 -4
  18. data/lib/active_model/secure_password.rb +2 -9
  19. data/lib/active_model/serialization.rb +36 -9
  20. data/lib/active_model/serializers/json.rb +1 -1
  21. data/lib/active_model/type.rb +59 -0
  22. data/lib/active_model/type/big_integer.rb +13 -0
  23. data/lib/active_model/type/binary.rb +50 -0
  24. data/lib/active_model/type/boolean.rb +21 -0
  25. data/lib/active_model/type/date.rb +50 -0
  26. data/lib/active_model/type/date_time.rb +44 -0
  27. data/lib/active_model/type/decimal.rb +52 -0
  28. data/lib/active_model/type/decimal_without_scale.rb +11 -0
  29. data/lib/active_model/type/float.rb +25 -0
  30. data/lib/active_model/type/helpers.rb +4 -0
  31. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +35 -0
  32. data/lib/active_model/type/helpers/mutable.rb +18 -0
  33. data/lib/active_model/type/helpers/numeric.rb +34 -0
  34. data/lib/active_model/type/helpers/time_value.rb +77 -0
  35. data/lib/active_model/type/immutable_string.rb +29 -0
  36. data/lib/active_model/type/integer.rb +66 -0
  37. data/lib/active_model/type/registry.rb +64 -0
  38. data/lib/active_model/type/string.rb +19 -0
  39. data/lib/active_model/type/text.rb +11 -0
  40. data/lib/active_model/type/time.rb +46 -0
  41. data/lib/active_model/type/unsigned_integer.rb +15 -0
  42. data/lib/active_model/type/value.rb +112 -0
  43. data/lib/active_model/validations.rb +35 -3
  44. data/lib/active_model/validations/absence.rb +1 -1
  45. data/lib/active_model/validations/acceptance.rb +61 -9
  46. data/lib/active_model/validations/callbacks.rb +3 -3
  47. data/lib/active_model/validations/confirmation.rb +16 -4
  48. data/lib/active_model/validations/exclusion.rb +3 -1
  49. data/lib/active_model/validations/format.rb +1 -1
  50. data/lib/active_model/validations/helper_methods.rb +13 -0
  51. data/lib/active_model/validations/inclusion.rb +3 -3
  52. data/lib/active_model/validations/length.rb +48 -17
  53. data/lib/active_model/validations/numericality.rb +12 -13
  54. data/lib/active_model/validations/validates.rb +1 -1
  55. data/lib/active_model/validations/with.rb +0 -10
  56. data/lib/active_model/validator.rb +6 -2
  57. data/lib/active_model/version.rb +1 -1
  58. metadata +34 -9
  59. data/lib/active_model/serializers/xml.rb +0 -238
@@ -0,0 +1,64 @@
1
+ module ActiveModel
2
+ # :stopdoc:
3
+ module Type
4
+ class Registry
5
+ def initialize
6
+ @registrations = []
7
+ end
8
+
9
+ def register(type_name, klass = nil, **options, &block)
10
+ block ||= proc { |_, *args| klass.new(*args) }
11
+ registrations << registration_klass.new(type_name, block, **options)
12
+ end
13
+
14
+ def lookup(symbol, *args)
15
+ registration = find_registration(symbol, *args)
16
+
17
+ if registration
18
+ registration.call(self, symbol, *args)
19
+ else
20
+ raise ArgumentError, "Unknown type #{symbol.inspect}"
21
+ end
22
+ end
23
+
24
+ protected
25
+
26
+ attr_reader :registrations
27
+
28
+ private
29
+
30
+ def registration_klass
31
+ Registration
32
+ end
33
+
34
+ def find_registration(symbol, *args)
35
+ registrations.find { |r| r.matches?(symbol, *args) }
36
+ end
37
+ end
38
+
39
+ class Registration
40
+ # Options must be taken because of https://bugs.ruby-lang.org/issues/10856
41
+ def initialize(name, block, **)
42
+ @name = name
43
+ @block = block
44
+ end
45
+
46
+ def call(_registry, *args, **kwargs)
47
+ if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
48
+ block.call(*args, **kwargs)
49
+ else
50
+ block.call(*args)
51
+ end
52
+ end
53
+
54
+ def matches?(type_name, *args, **kwargs)
55
+ type_name == name
56
+ end
57
+
58
+ protected
59
+
60
+ attr_reader :name, :block
61
+ end
62
+ end
63
+ # :startdoc:
64
+ end
@@ -0,0 +1,19 @@
1
+ require "active_model/type/immutable_string"
2
+
3
+ module ActiveModel
4
+ module Type
5
+ class String < ImmutableString # :nodoc:
6
+ def changed_in_place?(raw_old_value, new_value)
7
+ if new_value.is_a?(::String)
8
+ raw_old_value != new_value
9
+ end
10
+ end
11
+
12
+ private
13
+
14
+ def cast_value(value)
15
+ ::String.new(super)
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,11 @@
1
+ require 'active_model/type/string'
2
+
3
+ module ActiveModel
4
+ module Type
5
+ class Text < String # :nodoc:
6
+ def type
7
+ :text
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,46 @@
1
+ module ActiveModel
2
+ module Type
3
+ class Time < Value # :nodoc:
4
+ include Helpers::TimeValue
5
+ include Helpers::AcceptsMultiparameterTime.new(
6
+ defaults: { 1 => 1970, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
7
+ )
8
+
9
+ def type
10
+ :time
11
+ end
12
+
13
+ def user_input_in_time_zone(value)
14
+ return unless value.present?
15
+
16
+ case value
17
+ when ::String
18
+ value = "2000-01-01 #{value}"
19
+ when ::Time
20
+ value = value.change(year: 2000, day: 1, month: 1)
21
+ end
22
+
23
+ super(value)
24
+ end
25
+
26
+ private
27
+
28
+ def cast_value(value)
29
+ return value unless value.is_a?(::String)
30
+ return if value.empty?
31
+
32
+ if value =~ /^2000-01-01/
33
+ dummy_time_value = value
34
+ else
35
+ dummy_time_value = "2000-01-01 #{value}"
36
+ end
37
+
38
+ fast_string_to_time(dummy_time_value) || begin
39
+ time_hash = ::Date._parse(dummy_time_value)
40
+ return if time_hash[:hour].nil?
41
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,15 @@
1
+ module ActiveModel
2
+ module Type
3
+ class UnsignedInteger < Integer # :nodoc:
4
+ private
5
+
6
+ def max_value
7
+ super * 2
8
+ end
9
+
10
+ def min_value
11
+ 0
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,112 @@
1
+ module ActiveModel
2
+ module Type
3
+ class Value
4
+ attr_reader :precision, :scale, :limit
5
+
6
+ def initialize(precision: nil, limit: nil, scale: nil)
7
+ @precision = precision
8
+ @scale = scale
9
+ @limit = limit
10
+ end
11
+
12
+ def type # :nodoc:
13
+ end
14
+
15
+ # Converts a value from database input to the appropriate ruby type. The
16
+ # return value of this method will be returned from
17
+ # ActiveRecord::AttributeMethods::Read#read_attribute. The default
18
+ # implementation just calls Value#cast.
19
+ #
20
+ # +value+ The raw input, as provided from the database.
21
+ def deserialize(value)
22
+ cast(value)
23
+ end
24
+
25
+ # Type casts a value from user input (e.g. from a setter). This value may
26
+ # be a string from the form builder, or a ruby object passed to a setter.
27
+ # There is currently no way to differentiate between which source it came
28
+ # from.
29
+ #
30
+ # The return value of this method will be returned from
31
+ # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
32
+ # Value#cast_value.
33
+ #
34
+ # +value+ The raw input, as provided to the attribute setter.
35
+ def cast(value)
36
+ cast_value(value) unless value.nil?
37
+ end
38
+
39
+ # Casts a value from the ruby type to a type that the database knows how
40
+ # to understand. The returned value from this method should be a
41
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
42
+ # +nil+.
43
+ def serialize(value)
44
+ value
45
+ end
46
+
47
+ # Type casts a value for schema dumping. This method is private, as we are
48
+ # hoping to remove it entirely.
49
+ def type_cast_for_schema(value) # :nodoc:
50
+ value.inspect
51
+ end
52
+
53
+ # These predicates are not documented, as I need to look further into
54
+ # their use, and see if they can be removed entirely.
55
+ def binary? # :nodoc:
56
+ false
57
+ end
58
+
59
+ # Determines whether a value has changed for dirty checking. +old_value+
60
+ # and +new_value+ will always be type-cast. Types should not need to
61
+ # override this method.
62
+ def changed?(old_value, new_value, _new_value_before_type_cast)
63
+ old_value != new_value
64
+ end
65
+
66
+ # Determines whether the mutable value has been modified since it was
67
+ # read. Returns +false+ by default. If your type returns an object
68
+ # which could be mutated, you should override this method. You will need
69
+ # to either:
70
+ #
71
+ # - pass +new_value+ to Value#serialize and compare it to
72
+ # +raw_old_value+
73
+ #
74
+ # or
75
+ #
76
+ # - pass +raw_old_value+ to Value#deserialize and compare it to
77
+ # +new_value+
78
+ #
79
+ # +raw_old_value+ The original value, before being passed to
80
+ # +deserialize+.
81
+ #
82
+ # +new_value+ The current value, after type casting.
83
+ def changed_in_place?(raw_old_value, new_value)
84
+ false
85
+ end
86
+
87
+ def ==(other)
88
+ self.class == other.class &&
89
+ precision == other.precision &&
90
+ scale == other.scale &&
91
+ limit == other.limit
92
+ end
93
+ alias eql? ==
94
+
95
+ def hash
96
+ [self.class, precision, scale, limit].hash
97
+ end
98
+
99
+ def assert_valid_value(*)
100
+ end
101
+
102
+ private
103
+
104
+ # Convenience method for types which do not need separate type casting
105
+ # behavior for user and database inputs. Called by Value#cast for
106
+ # values except +nil+.
107
+ def cast_value(value) # :doc:
108
+ value
109
+ end
110
+ end
111
+ end
112
+ end
@@ -47,10 +47,9 @@ module ActiveModel
47
47
  include HelperMethods
48
48
 
49
49
  attr_accessor :validation_context
50
- private :validation_context=
51
50
  define_callbacks :validate, scope: :name
52
51
 
53
- class_attribute :_validators, instance_writer: false
52
+ class_attribute :_validators
54
53
  self._validators = Hash.new { |h,k| h[k] = [] }
55
54
  end
56
55
 
@@ -163,7 +162,7 @@ module ActiveModel
163
162
  options = options.dup
164
163
  options[:if] = Array(options[:if])
165
164
  options[:if].unshift ->(o) {
166
- Array(options[:on]).include?(o.validation_context)
165
+ !(Array(options[:on]) & Array(o.validation_context)).empty?
167
166
  }
168
167
  end
169
168
 
@@ -375,6 +374,15 @@ module ActiveModel
375
374
  !valid?(context)
376
375
  end
377
376
 
377
+ # Runs all the validations within the specified context. Returns +true+ if
378
+ # no errors are found, raises +ValidationError+ otherwise.
379
+ #
380
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
381
+ # some <tt>:on</tt> option will only run in the specified context.
382
+ def validate!(context = nil)
383
+ valid?(context) || raise_validation_error
384
+ end
385
+
378
386
  # Hook method defining how an attribute value should be retrieved. By default
379
387
  # this is assumed to be an instance named after the attribute. Override this
380
388
  # method in subclasses should you need to retrieve the value for a given
@@ -399,6 +407,30 @@ module ActiveModel
399
407
  _run_validate_callbacks
400
408
  errors.empty?
401
409
  end
410
+
411
+ def raise_validation_error
412
+ raise(ValidationError.new(self))
413
+ end
414
+ end
415
+
416
+ # = Active Model ValidationError
417
+ #
418
+ # Raised by <tt>validate!</tt> when the model is invalid. Use the
419
+ # +model+ method to retrieve the record which did not validate.
420
+ #
421
+ # begin
422
+ # complex_operation_that_internally_calls_validate!
423
+ # rescue ActiveModel::ValidationError => invalid
424
+ # puts invalid.model.errors
425
+ # end
426
+ class ValidationError < StandardError
427
+ attr_reader :model
428
+
429
+ def initialize(model)
430
+ @model = model
431
+ errors = @model.errors.full_messages.join(", ")
432
+ super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid"))
433
+ end
402
434
  end
403
435
  end
404
436
 
@@ -1,6 +1,6 @@
1
1
  module ActiveModel
2
2
  module Validations
3
- # == Active Model Absence Validator
3
+ # == \Active \Model Absence Validator
4
4
  class AbsenceValidator < EachValidator #:nodoc:
5
5
  def validate_each(record, attr_name, value)
6
6
  record.errors.add(attr_name, :present, options) if value.present?
@@ -3,22 +3,73 @@ module ActiveModel
3
3
  module Validations
4
4
  class AcceptanceValidator < EachValidator # :nodoc:
5
5
  def initialize(options)
6
- super({ allow_nil: true, accept: "1" }.merge!(options))
6
+ super({ allow_nil: true, accept: ["1", true] }.merge!(options))
7
7
  setup!(options[:class])
8
8
  end
9
9
 
10
10
  def validate_each(record, attribute, value)
11
- unless value == options[:accept]
11
+ unless acceptable_option?(value)
12
12
  record.errors.add(attribute, :accepted, options.except(:accept, :allow_nil))
13
13
  end
14
14
  end
15
15
 
16
16
  private
17
+
17
18
  def setup!(klass)
18
- attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
19
- attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
20
- klass.send(:attr_reader, *attr_readers)
21
- klass.send(:attr_writer, *attr_writers)
19
+ klass.include(LazilyDefineAttributes.new(AttributeDefinition.new(attributes)))
20
+ end
21
+
22
+ def acceptable_option?(value)
23
+ Array(options[:accept]).include?(value)
24
+ end
25
+
26
+ class LazilyDefineAttributes < Module
27
+ def initialize(attribute_definition)
28
+ define_method(:respond_to_missing?) do |method_name, include_private=false|
29
+ super(method_name, include_private) || attribute_definition.matches?(method_name)
30
+ end
31
+
32
+ define_method(:method_missing) do |method_name, *args, &block|
33
+ if attribute_definition.matches?(method_name)
34
+ attribute_definition.define_on(self.class)
35
+ send(method_name, *args, &block)
36
+ else
37
+ super(method_name, *args, &block)
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ class AttributeDefinition
44
+ def initialize(attributes)
45
+ @attributes = attributes.map(&:to_s)
46
+ end
47
+
48
+ def matches?(method_name)
49
+ attr_name = convert_to_reader_name(method_name)
50
+ attributes.include?(attr_name)
51
+ end
52
+
53
+ def define_on(klass)
54
+ attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
55
+ attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
56
+ klass.send(:attr_reader, *attr_readers)
57
+ klass.send(:attr_writer, *attr_writers)
58
+ end
59
+
60
+ protected
61
+
62
+ attr_reader :attributes
63
+
64
+ private
65
+
66
+ def convert_to_reader_name(method_name)
67
+ attr_name = method_name.to_s
68
+ if attr_name.end_with?("=")
69
+ attr_name = attr_name[0..-2]
70
+ end
71
+ attr_name
72
+ end
22
73
  end
23
74
  end
24
75
 
@@ -38,9 +89,10 @@ module ActiveModel
38
89
  # Configuration options:
39
90
  # * <tt>:message</tt> - A custom error message (default is: "must be
40
91
  # accepted").
41
- # * <tt>:accept</tt> - Specifies value that is considered accepted.
42
- # The default value is a string "1", which makes it easy to relate to
43
- # an HTML checkbox. This should be set to +true+ if you are validating
92
+ # * <tt>:accept</tt> - Specifies a value that is considered accepted.
93
+ # Also accepts an array of possible values. The default value is
94
+ # an array ["1", true], which makes it easy to relate to an HTML
95
+ # checkbox. This should be set to, or include, +true+ if you are validating
44
96
  # a database column, since the attribute is typecast from "1" to +true+
45
97
  # before validation.
46
98
  #
@@ -15,15 +15,15 @@ module ActiveModel
15
15
  # after_validation :do_stuff_after_validation
16
16
  # end
17
17
  #
18
- # Like other <tt>before_*</tt> callbacks if +before_validation+ returns
19
- # +false+ then <tt>valid?</tt> will not be called.
18
+ # Like other <tt>before_*</tt> callbacks if +before_validation+ throws
19
+ # +:abort+ then <tt>valid?</tt> will not be called.
20
20
  module Callbacks
21
21
  extend ActiveSupport::Concern
22
22
 
23
23
  included do
24
24
  include ActiveSupport::Callbacks
25
25
  define_callbacks :validation,
26
- terminator: ->(_,result) { result == false },
26
+ terminator: deprecated_false_terminator,
27
27
  skip_after_callbacks_if_terminated: true,
28
28
  scope: [:kind, :name]
29
29
  end