loading
Generated 2020-08-24T16:11:23-04:00

All Files ( 91.69% covered at 62.48 hits/line )

63 files in total.
2371 relevant lines, 2174 lines covered and 197 lines missed. ( 91.69% )
File % covered Lines Relevant Lines Lines covered Lines missed Avg. Hits / Line
lib/active_model.rb 95.00 % 78 40 38 2 0.95
lib/active_model/attribute.rb 84.50 % 248 129 109 20 24.67
lib/active_model/attribute/user_provided_default.rb 96.30 % 51 27 26 1 2.33
lib/active_model/attribute_assignment.rb 100.00 % 55 19 19 0 15.26
lib/active_model/attribute_methods.rb 89.51 % 555 143 128 15 110.90
lib/active_model/attribute_mutation_tracker.rb 89.25 % 181 93 83 10 21.41
lib/active_model/attribute_set.rb 91.53 % 106 59 54 5 18.25
lib/active_model/attribute_set/builder.rb 85.71 % 191 112 96 16 5.96
lib/active_model/attribute_set/yaml_encoder.rb 40.00 % 40 20 8 12 0.40
lib/active_model/attributes.rb 90.16 % 146 61 55 6 4.93
lib/active_model/callbacks.rb 100.00 % 155 32 32 0 4.97
lib/active_model/conversion.rb 100.00 % 111 18 18 0 5.33
lib/active_model/dirty.rb 91.78 % 288 73 67 6 9.23
lib/active_model/error.rb 97.80 % 201 91 89 2 183.36
lib/active_model/errors.rb 99.52 % 712 207 206 1 78.70
lib/active_model/forbidden_attributes_protection.rb 100.00 % 31 10 10 0 5.20
lib/active_model/gem_version.rb 88.89 % 17 9 8 1 0.89
lib/active_model/lint.rb 100.00 % 118 36 36 0 3.61
lib/active_model/model.rb 100.00 % 99 14 14 0 8.79
lib/active_model/naming.rb 100.00 % 333 61 61 0 147.69
lib/active_model/nested_error.rb 100.00 % 22 14 14 0 4.36
lib/active_model/railtie.rb 0.00 % 20 14 0 14 0.00
lib/active_model/secure_password.rb 94.44 % 128 36 34 2 5.78
lib/active_model/serialization.rb 100.00 % 192 27 27 0 28.07
lib/active_model/serializers/json.rb 100.00 % 154 23 23 0 4.70
lib/active_model/translation.rb 100.00 % 70 24 24 0 1355.46
lib/active_model/type.rb 100.00 % 53 36 36 0 1.86
lib/active_model/type/big_integer.rb 100.00 % 14 7 7 0 1.71
lib/active_model/type/binary.rb 59.26 % 52 27 16 11 0.74
lib/active_model/type/boolean.rb 83.33 % 46 12 10 2 7.25
lib/active_model/type/date.rb 89.66 % 52 29 26 3 1.90
lib/active_model/type/date_time.rb 95.83 % 46 24 23 1 2.38
lib/active_model/type/decimal.rb 91.18 % 69 34 31 3 3.65
lib/active_model/type/float.rb 52.38 % 35 21 11 10 1.29
lib/active_model/type/helpers.rb 100.00 % 7 5 5 0 1.00
lib/active_model/type/helpers/accepts_multiparameter_time.rb 80.77 % 49 26 21 5 3.69
lib/active_model/type/helpers/mutable.rb 75.00 % 20 8 6 2 0.75
lib/active_model/type/helpers/numeric.rb 100.00 % 48 23 23 0 19.74
lib/active_model/type/helpers/time_value.rb 65.22 % 90 46 30 16 1.63
lib/active_model/type/helpers/timezone.rb 100.00 % 19 9 9 0 2.33
lib/active_model/type/immutable_string.rb 71.43 % 35 21 15 6 6.52
lib/active_model/type/integer.rb 91.43 % 67 35 32 3 18.94
lib/active_model/type/registry.rb 94.44 % 70 36 34 2 11.69
lib/active_model/type/string.rb 93.75 % 35 16 15 1 10.94
lib/active_model/type/time.rb 92.00 % 46 25 23 2 2.08
lib/active_model/type/value.rb 82.50 % 133 40 33 7 20.98
lib/active_model/validations.rb 100.00 % 436 74 74 0 182.08
lib/active_model/validations/absence.rb 100.00 % 33 8 8 0 3.00
lib/active_model/validations/acceptance.rb 97.96 % 113 49 48 1 9.59
lib/active_model/validations/callbacks.rb 100.00 % 121 30 30 0 53.33
lib/active_model/validations/clusivity.rb 100.00 % 57 26 26 0 38.92
lib/active_model/validations/confirmation.rb 100.00 % 80 24 24 0 12.71
lib/active_model/validations/exclusion.rb 100.00 % 49 11 11 0 7.27
lib/active_model/validations/format.rb 100.00 % 113 34 34 0 12.88
lib/active_model/validations/helper_methods.rb 100.00 % 15 9 9 0 144.11
lib/active_model/validations/inclusion.rb 100.00 % 47 11 11 0 11.82
lib/active_model/validations/length.rb 95.24 % 129 42 40 2 66.21
lib/active_model/validations/numericality.rb 94.74 % 196 76 72 4 166.12
lib/active_model/validations/presence.rb 100.00 % 39 8 8 0 13.13
lib/active_model/validations/validates.rb 100.00 % 175 30 30 0 18.77
lib/active_model/validations/with.rb 100.00 % 147 26 26 0 114.58
lib/active_model/validator.rb 94.59 % 187 37 35 2 175.59
lib/active_model/version.rb 75.00 % 10 4 3 1 0.75

lib/active_model.rb

95.0% lines covered

40 relevant lines. 38 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. #--
  3. # Copyright (c) 2004-2020 David Heinemeier Hansson
  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. 1 require "active_support"
  25. 1 require "active_support/rails"
  26. 1 require "active_model/version"
  27. 1 module ActiveModel
  28. 1 extend ActiveSupport::Autoload
  29. 1 autoload :Attribute
  30. 1 autoload :Attributes
  31. 1 autoload :AttributeAssignment
  32. 1 autoload :AttributeMethods
  33. 1 autoload :BlockValidator, "active_model/validator"
  34. 1 autoload :Callbacks
  35. 1 autoload :Conversion
  36. 1 autoload :Dirty
  37. 1 autoload :EachValidator, "active_model/validator"
  38. 1 autoload :ForbiddenAttributesProtection
  39. 1 autoload :Lint
  40. 1 autoload :Model
  41. 1 autoload :Name, "active_model/naming"
  42. 1 autoload :Naming
  43. 1 autoload :SecurePassword
  44. 1 autoload :Serialization
  45. 1 autoload :Translation
  46. 1 autoload :Type
  47. 1 autoload :Validations
  48. 1 autoload :Validator
  49. 1 eager_autoload do
  50. 1 autoload :Errors
  51. 1 autoload :Error
  52. 1 autoload :RangeError, "active_model/errors"
  53. 1 autoload :StrictValidationFailed, "active_model/errors"
  54. 1 autoload :UnknownAttributeError, "active_model/errors"
  55. end
  56. 1 module Serializers
  57. 1 extend ActiveSupport::Autoload
  58. 1 eager_autoload do
  59. 1 autoload :JSON
  60. end
  61. end
  62. 1 def self.eager_load!
  63. super
  64. ActiveModel::Serializers.eager_load!
  65. end
  66. end
  67. 1 ActiveSupport.on_load(:i18n) do
  68. 1 I18n.load_path << File.expand_path("active_model/locale/en.yml", __dir__)
  69. end

lib/active_model/attribute.rb

84.5% lines covered

129 relevant lines. 109 lines covered and 20 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/duplicable"
  3. 1 module ActiveModel
  4. 1 class Attribute # :nodoc:
  5. 1 class << self
  6. 1 def from_database(name, value_before_type_cast, type, value = nil)
  7. 110 FromDatabase.new(name, value_before_type_cast, type, nil, value)
  8. end
  9. 1 def from_user(name, value_before_type_cast, type, original_attribute = nil)
  10. 65 FromUser.new(name, value_before_type_cast, type, original_attribute)
  11. end
  12. 1 def with_cast_value(name, value_before_type_cast, type)
  13. 9 WithCastValue.new(name, value_before_type_cast, type)
  14. end
  15. 1 def null(name)
  16. 11 Null.new(name)
  17. end
  18. 1 def uninitialized(name, type)
  19. 11 Uninitialized.new(name, type)
  20. end
  21. end
  22. 1 attr_reader :name, :value_before_type_cast, :type
  23. # This method should not be called directly.
  24. # Use #from_database or #from_user
  25. 1 def initialize(name, value_before_type_cast, type, original_attribute = nil, value = nil)
  26. 210 @name = name
  27. 210 @value_before_type_cast = value_before_type_cast
  28. 210 @type = type
  29. 210 @original_attribute = original_attribute
  30. 210 @value = value unless value.nil?
  31. end
  32. 1 def value
  33. # `defined?` is cheaper than `||=` when we get back falsy values
  34. 220 @value = type_cast(value_before_type_cast) unless defined?(@value)
  35. 220 @value
  36. end
  37. 1 def original_value
  38. 124 if assigned?
  39. 62 original_attribute.original_value
  40. else
  41. 62 type_cast(value_before_type_cast)
  42. end
  43. end
  44. 1 def value_for_database
  45. 49 type.serialize(value)
  46. end
  47. 1 def changed?
  48. 103 changed_from_assignment? || changed_in_place?
  49. end
  50. 1 def changed_in_place?
  51. 41 has_been_read? && type.changed_in_place?(original_value_for_database, value)
  52. end
  53. 1 def forgetting_assignment
  54. 47 with_value_from_database(value_for_database)
  55. end
  56. 1 def with_value_from_user(value)
  57. 61 type.assert_valid_value(value)
  58. 60 self.class.from_user(name, value, type, original_attribute || self)
  59. end
  60. 1 def with_value_from_database(value)
  61. 51 self.class.from_database(name, value, type)
  62. end
  63. 1 def with_cast_value(value)
  64. 2 self.class.with_cast_value(name, value, type)
  65. end
  66. 1 def with_type(type)
  67. 2 if changed_in_place?
  68. with_value_from_user(value).with_type(type)
  69. else
  70. 2 self.class.new(name, value_before_type_cast, type, original_attribute)
  71. end
  72. end
  73. 1 def type_cast(*)
  74. raise NotImplementedError
  75. end
  76. 1 def initialized?
  77. 126 true
  78. end
  79. 1 def came_from_user?
  80. false
  81. end
  82. 1 def has_been_read?
  83. 47 defined?(@value)
  84. end
  85. 1 def ==(other)
  86. 14 self.class == other.class &&
  87. name == other.name &&
  88. value_before_type_cast == other.value_before_type_cast &&
  89. type == other.type
  90. end
  91. 1 alias eql? ==
  92. 1 def hash
  93. [self.class, name, value_before_type_cast, type].hash
  94. end
  95. 1 def init_with(coder)
  96. @name = coder["name"]
  97. @value_before_type_cast = coder["value_before_type_cast"]
  98. @type = coder["type"]
  99. @original_attribute = coder["original_attribute"]
  100. @value = coder["value"] if coder.map.key?("value")
  101. end
  102. 1 def encode_with(coder)
  103. coder["name"] = name
  104. coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
  105. coder["type"] = type if type
  106. coder["original_attribute"] = original_attribute if original_attribute
  107. coder["value"] = value if defined?(@value)
  108. end
  109. 1 def original_value_for_database
  110. 13 if assigned?
  111. 4 original_attribute.original_value_for_database
  112. else
  113. 9 _original_value_for_database
  114. end
  115. end
  116. 1 private
  117. 1 attr_reader :original_attribute
  118. 1 alias :assigned? :original_attribute
  119. 1 def initialize_dup(other)
  120. 150 if defined?(@value) && @value.duplicable?
  121. 4 @value = @value.dup
  122. end
  123. end
  124. 1 def changed_from_assignment?
  125. 103 assigned? && type.changed?(original_value, value, value_before_type_cast)
  126. end
  127. 1 def _original_value_for_database
  128. type.serialize(original_value)
  129. end
  130. 1 class FromDatabase < Attribute # :nodoc:
  131. 1 def type_cast(value)
  132. 68 type.deserialize(value)
  133. end
  134. 1 def _original_value_for_database
  135. 9 value_before_type_cast
  136. end
  137. 1 private :_original_value_for_database
  138. end
  139. 1 class FromUser < Attribute # :nodoc:
  140. 1 def type_cast(value)
  141. 70 type.cast(value)
  142. end
  143. 1 def came_from_user?
  144. !type.value_constructed_by_mass_assignment?(value_before_type_cast)
  145. end
  146. end
  147. 1 class WithCastValue < Attribute # :nodoc:
  148. 1 def type_cast(value)
  149. 69 value
  150. end
  151. 1 def changed_in_place?
  152. 28 false
  153. end
  154. end
  155. 1 class Null < Attribute # :nodoc:
  156. 1 def initialize(name)
  157. 11 super(name, nil, Type.default_value)
  158. end
  159. 1 def type_cast(*)
  160. nil
  161. end
  162. 1 def with_type(type)
  163. 7 self.class.with_cast_value(name, nil, type)
  164. end
  165. 1 def with_value_from_database(value)
  166. raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
  167. end
  168. 1 alias_method :with_value_from_user, :with_value_from_database
  169. 1 alias_method :with_cast_value, :with_value_from_database
  170. end
  171. 1 class Uninitialized < Attribute # :nodoc:
  172. 1 UNINITIALIZED_ORIGINAL_VALUE = Object.new
  173. 1 def initialize(name, type)
  174. 11 super(name, nil, type)
  175. end
  176. 1 def value
  177. 7 if block_given?
  178. 3 yield name
  179. end
  180. end
  181. 1 def original_value
  182. UNINITIALIZED_ORIGINAL_VALUE
  183. end
  184. 1 def value_for_database
  185. end
  186. 1 def initialized?
  187. 4 false
  188. end
  189. 1 def forgetting_assignment
  190. dup
  191. end
  192. 1 def with_type(type)
  193. self.class.new(name, type)
  194. end
  195. end
  196. 1 private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
  197. end
  198. end

lib/active_model/attribute/user_provided_default.rb

96.3% lines covered

27 relevant lines. 26 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/attribute"
  3. 1 module ActiveModel
  4. 1 class Attribute # :nodoc:
  5. 1 class UserProvidedDefault < FromUser # :nodoc:
  6. 1 def initialize(name, value, type, database_default)
  7. 2 @user_provided_value = value
  8. 2 super(name, value, type, database_default)
  9. end
  10. 1 def value_before_type_cast
  11. 14 if user_provided_value.is_a?(Proc)
  12. 6 @memoized_value_before_type_cast ||= user_provided_value.call
  13. else
  14. 8 @user_provided_value
  15. end
  16. end
  17. 1 def with_type(type)
  18. self.class.new(name, user_provided_value, type, original_attribute)
  19. end
  20. 1 def marshal_dump
  21. 2 result = [
  22. name,
  23. value_before_type_cast,
  24. type,
  25. original_attribute,
  26. ]
  27. 2 result << value if defined?(@value)
  28. 2 result
  29. end
  30. 1 def marshal_load(values)
  31. 2 name, user_provided_value, type, original_attribute, value = values
  32. 2 @name = name
  33. 2 @user_provided_value = user_provided_value
  34. 2 @type = type
  35. 2 @original_attribute = original_attribute
  36. 2 if values.length == 5
  37. 2 @value = value
  38. end
  39. end
  40. 1 private
  41. 1 attr_reader :user_provided_value
  42. end
  43. end
  44. end

lib/active_model/attribute_assignment.rb

100.0% lines covered

19 relevant lines. 19 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/keys"
  3. 1 module ActiveModel
  4. 1 module AttributeAssignment
  5. 1 include ActiveModel::ForbiddenAttributesProtection
  6. # Allows you to set all the attributes by passing in a hash of attributes with
  7. # keys matching the attribute names.
  8. #
  9. # If the passed hash responds to <tt>permitted?</tt> method and the return value
  10. # of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
  11. # exception is raised.
  12. #
  13. # class Cat
  14. # include ActiveModel::AttributeAssignment
  15. # attr_accessor :name, :status
  16. # end
  17. #
  18. # cat = Cat.new
  19. # cat.assign_attributes(name: "Gorby", status: "yawning")
  20. # cat.name # => 'Gorby'
  21. # cat.status # => 'yawning'
  22. # cat.assign_attributes(status: "sleeping")
  23. # cat.name # => 'Gorby'
  24. # cat.status # => 'sleeping'
  25. 1 def assign_attributes(new_attributes)
  26. 66 unless new_attributes.respond_to?(:each_pair)
  27. 1 raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed."
  28. end
  29. 65 return if new_attributes.empty?
  30. 19 _assign_attributes(sanitize_for_mass_assignment(new_attributes))
  31. end
  32. 1 alias attributes= assign_attributes
  33. 1 private
  34. 1 def _assign_attributes(attributes)
  35. 18 attributes.each do |k, v|
  36. 28 _assign_attribute(k, v)
  37. end
  38. end
  39. 1 def _assign_attribute(k, v)
  40. 28 setter = :"#{k}="
  41. 28 if respond_to?(setter)
  42. 24 public_send(setter, v)
  43. else
  44. 4 raise UnknownAttributeError.new(self, k.to_s)
  45. end
  46. end
  47. end
  48. end

lib/active_model/attribute_methods.rb

89.51% lines covered

143 relevant lines. 128 lines covered and 15 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "concurrent/map"
  3. 1 module ActiveModel
  4. # Raised when an attribute is not defined.
  5. #
  6. # class User < ActiveRecord::Base
  7. # has_many :pets
  8. # end
  9. #
  10. # user = User.first
  11. # user.pets.select(:id).first.user_id
  12. # # => ActiveModel::MissingAttributeError: missing attribute: user_id
  13. 1 class MissingAttributeError < NoMethodError
  14. end
  15. # == Active \Model \Attribute \Methods
  16. #
  17. # Provides a way to add prefixes and suffixes to your methods as
  18. # well as handling the creation of <tt>ActiveRecord::Base</tt>-like
  19. # class methods such as +table_name+.
  20. #
  21. # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
  22. #
  23. # * <tt>include ActiveModel::AttributeMethods</tt> in your class.
  24. # * Call each of its methods you want to add, such as +attribute_method_suffix+
  25. # or +attribute_method_prefix+.
  26. # * Call +define_attribute_methods+ after the other methods are called.
  27. # * Define the various generic +_attribute+ methods that you have declared.
  28. # * Define an +attributes+ method which returns a hash with each
  29. # attribute name in your model as hash key and the attribute value as hash value.
  30. # Hash keys must be strings.
  31. #
  32. # A minimal implementation could be:
  33. #
  34. # class Person
  35. # include ActiveModel::AttributeMethods
  36. #
  37. # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
  38. # attribute_method_suffix '_contrived?'
  39. # attribute_method_prefix 'clear_'
  40. # define_attribute_methods :name
  41. #
  42. # attr_accessor :name
  43. #
  44. # def attributes
  45. # { 'name' => @name }
  46. # end
  47. #
  48. # private
  49. #
  50. # def attribute_contrived?(attr)
  51. # true
  52. # end
  53. #
  54. # def clear_attribute(attr)
  55. # send("#{attr}=", nil)
  56. # end
  57. #
  58. # def reset_attribute_to_default!(attr)
  59. # send("#{attr}=", 'Default Name')
  60. # end
  61. # end
  62. 1 module AttributeMethods
  63. 1 extend ActiveSupport::Concern
  64. 1 NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
  65. 1 CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
  66. 1 included do
  67. 10 class_attribute :attribute_aliases, instance_writer: false, default: {}
  68. 10 class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
  69. end
  70. 1 module ClassMethods
  71. # Declares a method available for all attributes with the given prefix.
  72. # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
  73. #
  74. # #{prefix}#{attr}(*args, &block)
  75. #
  76. # to
  77. #
  78. # #{prefix}attribute(#{attr}, *args, &block)
  79. #
  80. # An instance method <tt>#{prefix}attribute</tt> must exist and accept
  81. # at least the +attr+ argument.
  82. #
  83. # class Person
  84. # include ActiveModel::AttributeMethods
  85. #
  86. # attr_accessor :name
  87. # attribute_method_prefix 'clear_'
  88. # define_attribute_methods :name
  89. #
  90. # private
  91. #
  92. # def clear_attribute(attr)
  93. # send("#{attr}=", nil)
  94. # end
  95. # end
  96. #
  97. # person = Person.new
  98. # person.name = 'Bob'
  99. # person.name # => "Bob"
  100. # person.clear_name
  101. # person.name # => nil
  102. 1 def attribute_method_prefix(*prefixes)
  103. self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
  104. undefine_attribute_methods
  105. end
  106. # Declares a method available for all attributes with the given suffix.
  107. # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
  108. #
  109. # #{attr}#{suffix}(*args, &block)
  110. #
  111. # to
  112. #
  113. # attribute#{suffix}(#{attr}, *args, &block)
  114. #
  115. # An <tt>attribute#{suffix}</tt> instance method must exist and accept at
  116. # least the +attr+ argument.
  117. #
  118. # class Person
  119. # include ActiveModel::AttributeMethods
  120. #
  121. # attr_accessor :name
  122. # attribute_method_suffix '_short?'
  123. # define_attribute_methods :name
  124. #
  125. # private
  126. #
  127. # def attribute_short?(attr)
  128. # send(attr).length < 5
  129. # end
  130. # end
  131. #
  132. # person = Person.new
  133. # person.name = 'Bob'
  134. # person.name # => "Bob"
  135. # person.name_short? # => true
  136. 1 def attribute_method_suffix(*suffixes)
  137. 27 self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
  138. 8 undefine_attribute_methods
  139. end
  140. # Declares a method available for all attributes with the given prefix
  141. # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
  142. # the method.
  143. #
  144. # #{prefix}#{attr}#{suffix}(*args, &block)
  145. #
  146. # to
  147. #
  148. # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
  149. #
  150. # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
  151. # accept at least the +attr+ argument.
  152. #
  153. # class Person
  154. # include ActiveModel::AttributeMethods
  155. #
  156. # attr_accessor :name
  157. # attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
  158. # define_attribute_methods :name
  159. #
  160. # private
  161. #
  162. # def reset_attribute_to_default!(attr)
  163. # send("#{attr}=", 'Default Name')
  164. # end
  165. # end
  166. #
  167. # person = Person.new
  168. # person.name # => 'Gem'
  169. # person.reset_name_to_default!
  170. # person.name # => 'Default Name'
  171. 1 def attribute_method_affix(*affixes)
  172. 8 self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
  173. 4 undefine_attribute_methods
  174. end
  175. # Allows you to make aliases for attributes.
  176. #
  177. # class Person
  178. # include ActiveModel::AttributeMethods
  179. #
  180. # attr_accessor :name
  181. # attribute_method_suffix '_short?'
  182. # define_attribute_methods :name
  183. #
  184. # alias_attribute :nickname, :name
  185. #
  186. # private
  187. #
  188. # def attribute_short?(attr)
  189. # send(attr).length < 5
  190. # end
  191. # end
  192. #
  193. # person = Person.new
  194. # person.name = 'Bob'
  195. # person.name # => "Bob"
  196. # person.nickname # => "Bob"
  197. # person.name_short? # => true
  198. # person.nickname_short? # => true
  199. 1 def alias_attribute(new_name, old_name)
  200. 4 self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
  201. 4 CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
  202. 4 attribute_method_matchers.each do |matcher|
  203. 4 matcher_new = matcher.method_name(new_name).to_s
  204. 4 matcher_old = matcher.method_name(old_name).to_s
  205. 4 define_proxy_call false, owner, matcher_new, matcher_old
  206. end
  207. end
  208. end
  209. # Is +new_name+ an alias?
  210. 1 def attribute_alias?(new_name)
  211. attribute_aliases.key? new_name.to_s
  212. end
  213. # Returns the original name for the alias +name+
  214. 1 def attribute_alias(name)
  215. attribute_aliases[name.to_s]
  216. end
  217. # Declares the attributes that should be prefixed and suffixed by
  218. # <tt>ActiveModel::AttributeMethods</tt>.
  219. #
  220. # To use, pass attribute names (as strings or symbols). Be sure to declare
  221. # +define_attribute_methods+ after you define any prefix, suffix or affix
  222. # methods, or they will not hook in.
  223. #
  224. # class Person
  225. # include ActiveModel::AttributeMethods
  226. #
  227. # attr_accessor :name, :age, :address
  228. # attribute_method_prefix 'clear_'
  229. #
  230. # # Call to define_attribute_methods must appear after the
  231. # # attribute_method_prefix, attribute_method_suffix or
  232. # # attribute_method_affix declarations.
  233. # define_attribute_methods :name, :age, :address
  234. #
  235. # private
  236. #
  237. # def clear_attribute(attr)
  238. # send("#{attr}=", nil)
  239. # end
  240. # end
  241. 1 def define_attribute_methods(*attr_names)
  242. 9 CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
  243. 23 attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
  244. end
  245. end
  246. # Declares an attribute that should be prefixed and suffixed by
  247. # <tt>ActiveModel::AttributeMethods</tt>.
  248. #
  249. # To use, pass an attribute name (as string or symbol). Be sure to declare
  250. # +define_attribute_method+ after you define any prefix, suffix or affix
  251. # method, or they will not hook in.
  252. #
  253. # class Person
  254. # include ActiveModel::AttributeMethods
  255. #
  256. # attr_accessor :name
  257. # attribute_method_suffix '_short?'
  258. #
  259. # # Call to define_attribute_method must appear after the
  260. # # attribute_method_prefix, attribute_method_suffix or
  261. # # attribute_method_affix declarations.
  262. # define_attribute_method :name
  263. #
  264. # private
  265. #
  266. # def attribute_short?(attr)
  267. # send(attr).length < 5
  268. # end
  269. # end
  270. #
  271. # person = Person.new
  272. # person.name = 'Bob'
  273. # person.name # => "Bob"
  274. # person.name_short? # => true
  275. 1 def define_attribute_method(attr_name, _owner: generated_attribute_methods)
  276. 29 CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
  277. 29 attribute_method_matchers.each do |matcher|
  278. 105 method_name = matcher.method_name(attr_name)
  279. 105 unless instance_method_already_implemented?(method_name)
  280. 104 generate_method = "define_method_#{matcher.target}"
  281. 104 if respond_to?(generate_method, true)
  282. 10 send(generate_method, attr_name.to_s, owner: owner)
  283. else
  284. 94 define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
  285. end
  286. end
  287. end
  288. 29 attribute_method_matchers_cache.clear
  289. end
  290. end
  291. # Removes all the previously dynamically defined methods from the class.
  292. #
  293. # class Person
  294. # include ActiveModel::AttributeMethods
  295. #
  296. # attr_accessor :name
  297. # attribute_method_suffix '_short?'
  298. # define_attribute_method :name
  299. #
  300. # private
  301. #
  302. # def attribute_short?(attr)
  303. # send(attr).length < 5
  304. # end
  305. # end
  306. #
  307. # person = Person.new
  308. # person.name = 'Bob'
  309. # person.name_short? # => true
  310. #
  311. # Person.undefine_attribute_methods
  312. #
  313. # person.name_short? # => NoMethodError
  314. 1 def undefine_attribute_methods
  315. 21 generated_attribute_methods.module_eval do
  316. 21 undef_method(*instance_methods)
  317. end
  318. 21 attribute_method_matchers_cache.clear
  319. end
  320. 1 private
  321. 1 class CodeGenerator
  322. 1 class << self
  323. 1 def batch(owner, path, line)
  324. 42 if owner.is_a?(CodeGenerator)
  325. 14 yield owner
  326. else
  327. 28 instance = new(owner, path, line)
  328. 28 result = yield instance
  329. 28 instance.execute
  330. 28 result
  331. end
  332. end
  333. end
  334. 1 def initialize(owner, path, line)
  335. 28 @owner = owner
  336. 28 @path = path
  337. 28 @line = line
  338. 28 @sources = ["# frozen_string_literal: true\n"]
  339. 28 @renames = {}
  340. end
  341. 1 def <<(source_line)
  342. 108 @sources << source_line
  343. end
  344. 1 def rename_method(old_name, new_name)
  345. @renames[old_name] = new_name
  346. end
  347. 1 def execute
  348. 28 @owner.module_eval(@sources.join(";"), @path, @line - 1)
  349. 28 @renames.each do |old_name, new_name|
  350. @owner.alias_method new_name, old_name
  351. @owner.undef_method old_name
  352. end
  353. end
  354. end
  355. 1 private_constant :CodeGenerator
  356. 1 def generated_attribute_methods
  357. 164 @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
  358. end
  359. 1 def instance_method_already_implemented?(method_name)
  360. 105 generated_attribute_methods.method_defined?(method_name)
  361. end
  362. # The methods +method_missing+ and +respond_to?+ of this module are
  363. # invoked often in a typical rails, both of which invoke the method
  364. # +matched_attribute_method+. The latter method iterates through an
  365. # array doing regular expression matches, which results in a lot of
  366. # object creations. Most of the time it returns a +nil+ match. As the
  367. # match result is always the same given a +method_name+, this cache is
  368. # used to alleviate the GC, which ultimately also speeds up the app
  369. # significantly (in our case our test suite finishes 10% faster with
  370. # this cache).
  371. 1 def attribute_method_matchers_cache
  372. 1409 @attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
  373. end
  374. 1 def attribute_method_matchers_matching(method_name)
  375. 1359 attribute_method_matchers_cache.compute_if_absent(method_name) do
  376. 44 attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
  377. end
  378. end
  379. # Define a method `name` in `mod` that dispatches to `send`
  380. # using the given `extra` args. This falls back on `define_method`
  381. # and `send` if the given names cannot be compiled.
  382. 1 def define_proxy_call(include_private, code_generator, name, target, *extra)
  383. 98 defn = if NAME_COMPILABLE_REGEXP.match?(name)
  384. 95 "def #{name}(*args)"
  385. else
  386. 3 "define_method(:'#{name}') do |*args|"
  387. end
  388. 98 extra = (extra.map!(&:inspect) << "*args").join(", ")
  389. 98 body = if CALL_COMPILABLE_REGEXP.match?(target)
  390. 97 "#{"self." unless include_private}#{target}(#{extra})"
  391. else
  392. 1 "send(:'#{target}', #{extra})"
  393. end
  394. code_generator <<
  395. defn <<
  396. body <<
  397. 98 "end" <<
  398. "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
  399. end
  400. 1 class AttributeMethodMatcher #:nodoc:
  401. 1 attr_reader :prefix, :suffix, :target
  402. 1 AttributeMethodMatch = Struct.new(:target, :attr_name)
  403. 1 def initialize(options = {})
  404. 33 @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
  405. 33 @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
  406. 33 @target = "#{@prefix}attribute#{@suffix}"
  407. 33 @method_name = "#{prefix}%s#{suffix}"
  408. end
  409. 1 def match(method_name)
  410. 30 if @regex =~ method_name
  411. 19 AttributeMethodMatch.new(target, $1)
  412. end
  413. end
  414. 1 def method_name(attr_name)
  415. 113 @method_name % attr_name
  416. end
  417. end
  418. end
  419. # Allows access to the object attributes, which are held in the hash
  420. # returned by <tt>attributes</tt>, as though they were first-class
  421. # methods. So a +Person+ class with a +name+ attribute can for example use
  422. # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
  423. # the attributes hash -- except for multiple assignments with
  424. # <tt>ActiveRecord::Base#attributes=</tt>.
  425. #
  426. # It's also possible to instantiate related objects, so a <tt>Client</tt>
  427. # class belonging to the +clients+ table with a +master_id+ foreign key
  428. # can instantiate master through <tt>Client#master</tt>.
  429. 1 def method_missing(method, *args, &block)
  430. 9 if respond_to_without_attributes?(method, true)
  431. 2 super
  432. else
  433. 7 match = matched_attribute_method(method.to_s)
  434. 7 match ? attribute_missing(match, *args, &block) : super
  435. end
  436. end
  437. 1 ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
  438. # +attribute_missing+ is like +method_missing+, but for attributes. When
  439. # +method_missing+ is called we check to see if there is a matching
  440. # attribute method. If so, we tell +attribute_missing+ to dispatch the
  441. # attribute. This method can be overloaded to customize the behavior.
  442. 1 def attribute_missing(match, *args, &block)
  443. 3 __send__(match.target, match.attr_name, *args, &block)
  444. end
  445. # A +Person+ instance with a +name+ attribute can ask
  446. # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
  447. # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
  448. 1 alias :respond_to_without_attributes? :respond_to?
  449. 1 def respond_to?(method, include_private_methods = false)
  450. 1379 if super
  451. 25 true
  452. 1354 elsif !include_private_methods && super(method, true)
  453. # If we're here then we haven't found among non-private methods
  454. # but found among all methods. Which means that the given method is private.
  455. 2 false
  456. else
  457. 1352 !matched_attribute_method(method.to_s).nil?
  458. end
  459. end
  460. 1 private
  461. 1 def attribute_method?(attr_name)
  462. 1811 respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
  463. end
  464. # Returns a struct representing the matching attribute method.
  465. # The struct's attributes are prefix, base and suffix.
  466. 1 def matched_attribute_method(method_name)
  467. 1359 matches = self.class.send(:attribute_method_matchers_matching, method_name)
  468. 3170 matches.detect { |match| attribute_method?(match.attr_name) }
  469. end
  470. 1 def missing_attribute(attr_name, stack)
  471. raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
  472. end
  473. 1 def _read_attribute(attr)
  474. 67 __send__(attr)
  475. end
  476. 1 module AttrNames # :nodoc:
  477. 1 DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
  478. # We want to generate the methods via module_eval rather than
  479. # define_method, because define_method is slower on dispatch.
  480. # Evaluating many similar methods may use more memory as the instruction
  481. # sequences are duplicated and cached (in MRI). define_method may
  482. # be slower on dispatch, but if you're careful about the closure
  483. # created, then define_method will consume much less memory.
  484. #
  485. # But sometimes the database might return columns with
  486. # characters that are not allowed in normal method names (like
  487. # 'my_column(omg)'. So to work around this we first define with
  488. # the __temp__ identifier, and then use alias method to rename
  489. # it to what we want.
  490. #
  491. # We are also defining a constant to hold the frozen string of
  492. # the attribute name. Using a constant means that we do not have
  493. # to allocate an object on each call to the attribute method.
  494. # Making it frozen means that it doesn't get duped when used to
  495. # key the @attributes in read_attribute.
  496. 1 def self.define_attribute_accessor_method(owner, attr_name, writer: false)
  497. 10 method_name = "#{attr_name}#{'=' if writer}"
  498. 10 if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
  499. 10 yield method_name, "'#{attr_name}'"
  500. else
  501. safe_name = attr_name.unpack1("h*")
  502. const_name = "ATTR_#{safe_name}"
  503. const_set(const_name, attr_name) unless const_defined?(const_name)
  504. temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
  505. attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
  506. yield temp_method_name, attr_name_expr
  507. owner.rename_method(temp_method_name, method_name)
  508. end
  509. end
  510. end
  511. end
  512. end

lib/active_model/attribute_mutation_tracker.rb

89.25% lines covered

93 relevant lines. 83 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/indifferent_access"
  3. 1 require "active_support/core_ext/object/duplicable"
  4. 1 module ActiveModel
  5. 1 class AttributeMutationTracker # :nodoc:
  6. 1 OPTION_NOT_GIVEN = Object.new
  7. 1 def initialize(attributes)
  8. 61 @attributes = attributes
  9. end
  10. 1 def changed_attribute_names
  11. 18 attr_names.select { |attr_name| changed?(attr_name) }
  12. end
  13. 1 def changed_values
  14. 10 attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
  15. 20 if changed?(attr_name)
  16. 8 result[attr_name] = original_value(attr_name)
  17. end
  18. end
  19. end
  20. 1 def changes
  21. 27 attr_names.each_with_object({}.with_indifferent_access) do |attr_name, result|
  22. 49 if change = change_to_attribute(attr_name)
  23. 34 result.merge!(attr_name => change)
  24. end
  25. end
  26. end
  27. 1 def change_to_attribute(attr_name)
  28. 46 if changed?(attr_name)
  29. 31 [original_value(attr_name), fetch_value(attr_name)]
  30. end
  31. end
  32. 1 def any_changes?
  33. 46 attr_names.any? { |attr| changed?(attr) }
  34. end
  35. 1 def changed?(attr_name, from: OPTION_NOT_GIVEN, to: OPTION_NOT_GIVEN)
  36. 203 attribute_changed?(attr_name) &&
  37. 136 (OPTION_NOT_GIVEN == from || original_value(attr_name) == from) &&
  38. 130 (OPTION_NOT_GIVEN == to || fetch_value(attr_name) == to)
  39. end
  40. 1 def changed_in_place?(attr_name)
  41. attributes[attr_name].changed_in_place?
  42. end
  43. 1 def forget_change(attr_name)
  44. 4 attributes[attr_name] = attributes[attr_name].forgetting_assignment
  45. 4 forced_changes.delete(attr_name)
  46. end
  47. 1 def original_value(attr_name)
  48. 20 attributes[attr_name].original_value
  49. end
  50. 1 def force_change(attr_name)
  51. forced_changes[attr_name] = fetch_value(attr_name)
  52. end
  53. 1 private
  54. 1 attr_reader :attributes
  55. 1 def forced_changes
  56. 370 @forced_changes ||= {}
  57. end
  58. 1 def attr_names
  59. 25 attributes.keys
  60. end
  61. 1 def attribute_changed?(attr_name)
  62. 97 forced_changes.include?(attr_name) || !!attributes[attr_name].changed?
  63. end
  64. 1 def fetch_value(attr_name)
  65. 11 attributes.fetch_value(attr_name)
  66. end
  67. end
  68. 1 class ForcedMutationTracker < AttributeMutationTracker # :nodoc:
  69. 1 def initialize(attributes)
  70. 30 super
  71. 30 @finalized_changes = nil
  72. end
  73. 1 def changed_in_place?(attr_name)
  74. false
  75. end
  76. 1 def change_to_attribute(attr_name)
  77. 31 if finalized_changes&.include?(attr_name)
  78. 8 finalized_changes[attr_name].dup
  79. else
  80. 23 super
  81. end
  82. end
  83. 1 def forget_change(attr_name)
  84. 4 forced_changes.delete(attr_name)
  85. end
  86. 1 def original_value(attr_name)
  87. 41 if changed?(attr_name)
  88. 41 forced_changes[attr_name]
  89. else
  90. fetch_value(attr_name)
  91. end
  92. end
  93. 1 def force_change(attr_name)
  94. 44 forced_changes[attr_name] = clone_value(attr_name) unless attribute_changed?(attr_name)
  95. end
  96. 1 def finalize_changes
  97. 12 @finalized_changes = changes
  98. end
  99. 1 private
  100. 1 attr_reader :finalized_changes
  101. 1 def attr_names
  102. 36 forced_changes.keys
  103. end
  104. 1 def attribute_changed?(attr_name)
  105. 150 forced_changes.include?(attr_name)
  106. end
  107. 1 def fetch_value(attr_name)
  108. 67 attributes.send(:_read_attribute, attr_name)
  109. end
  110. 1 def clone_value(attr_name)
  111. 38 value = fetch_value(attr_name)
  112. 38 value.duplicable? ? value.clone : value
  113. rescue TypeError, NoMethodError
  114. value
  115. end
  116. end
  117. 1 class NullMutationTracker # :nodoc:
  118. 1 include Singleton
  119. 1 def changed_attribute_names
  120. []
  121. end
  122. 1 def changed_values
  123. {}
  124. end
  125. 1 def changes
  126. 2 {}
  127. end
  128. 1 def change_to_attribute(attr_name)
  129. end
  130. 1 def any_changes?
  131. false
  132. end
  133. 1 def changed?(attr_name, **)
  134. false
  135. end
  136. 1 def changed_in_place?(attr_name)
  137. false
  138. end
  139. 1 def original_value(attr_name)
  140. end
  141. end
  142. end

lib/active_model/attribute_set.rb

91.53% lines covered

59 relevant lines. 54 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/enumerable"
  3. 1 require "active_support/core_ext/object/deep_dup"
  4. 1 require "active_model/attribute_set/builder"
  5. 1 require "active_model/attribute_set/yaml_encoder"
  6. 1 module ActiveModel
  7. 1 class AttributeSet # :nodoc:
  8. 1 delegate :each_value, :fetch, :except, to: :attributes
  9. 1 def initialize(attributes)
  10. 87 @attributes = attributes
  11. end
  12. 1 def [](name)
  13. 448 @attributes[name] || default_attribute(name)
  14. end
  15. 1 def []=(name, value)
  16. 14 @attributes[name] = value
  17. end
  18. 1 def values_before_type_cast
  19. 1 attributes.transform_values(&:value_before_type_cast)
  20. end
  21. 1 def to_hash
  22. 50 keys.index_with { |name| self[name].value }
  23. end
  24. 1 alias :to_h :to_hash
  25. 1 def key?(name)
  26. attributes.key?(name) && self[name].initialized?
  27. end
  28. 1 def keys
  29. 144 attributes.each_key.select { |name| self[name].initialized? }
  30. end
  31. 1 def fetch_value(name, &block)
  32. 34 self[name].value(&block)
  33. end
  34. 1 def write_from_database(name, value)
  35. 3 @attributes[name] = self[name].with_value_from_database(value)
  36. end
  37. 1 def write_from_user(name, value)
  38. 56 raise FrozenError, "can't modify frozen attributes" if frozen?
  39. 55 @attributes[name] = self[name].with_value_from_user(value)
  40. 55 value
  41. end
  42. 1 def write_cast_value(name, value)
  43. @attributes[name] = self[name].with_cast_value(value)
  44. value
  45. end
  46. 1 def freeze
  47. 3 attributes.freeze
  48. 3 super
  49. end
  50. 1 def deep_dup
  51. 41 AttributeSet.new(attributes.deep_dup)
  52. end
  53. 1 def initialize_dup(_)
  54. 1 @attributes = @attributes.dup
  55. 1 super
  56. end
  57. 1 def initialize_clone(_)
  58. 2 @attributes = @attributes.clone
  59. 2 super
  60. end
  61. 1 def reset(key)
  62. if key?(key)
  63. write_from_database(key, nil)
  64. end
  65. end
  66. 1 def accessed
  67. 6 attributes.each_key.select { |name| self[name].has_been_read? }
  68. end
  69. 1 def map(&block)
  70. 15 new_attributes = attributes.transform_values(&block)
  71. 15 AttributeSet.new(new_attributes)
  72. end
  73. 1 def ==(other)
  74. 3 attributes == other.attributes
  75. end
  76. 1 protected
  77. 1 attr_reader :attributes
  78. 1 private
  79. 1 def default_attribute(name)
  80. 7 Attribute.null(name)
  81. end
  82. end
  83. end

lib/active_model/attribute_set/builder.rb

85.71% lines covered

112 relevant lines. 96 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/attribute"
  3. 1 module ActiveModel
  4. 1 class AttributeSet # :nodoc:
  5. 1 class Builder # :nodoc:
  6. 1 attr_reader :types, :default_attributes
  7. 1 def initialize(types, default_attributes = {})
  8. 26 @types = types
  9. 26 @default_attributes = default_attributes
  10. end
  11. 1 def build_from_database(values = {}, additional_types = {})
  12. 27 LazyAttributeSet.new(values, types, additional_types, default_attributes)
  13. end
  14. end
  15. end
  16. 1 class LazyAttributeSet < AttributeSet # :nodoc:
  17. 1 def initialize(values, types, additional_types, default_attributes, attributes = {})
  18. 27 super(attributes)
  19. 27 @values = values
  20. 27 @types = types
  21. 27 @additional_types = additional_types
  22. 27 @default_attributes = default_attributes
  23. 27 @casted_values = {}
  24. 27 @materialized = false
  25. end
  26. 1 def key?(name)
  27. 4 (values.key?(name) || types.key?(name) || @attributes.key?(name)) && self[name].initialized?
  28. end
  29. 1 def keys
  30. 7 keys = values.keys | types.keys | @attributes.keys
  31. 19 keys.keep_if { |name| self[name].initialized? }
  32. end
  33. 1 def fetch_value(name, &block)
  34. 11 if attr = @attributes[name]
  35. 3 return attr.value(&block)
  36. end
  37. 8 @casted_values.fetch(name) do
  38. 8 value_present = true
  39. 14 value = values.fetch(name) { value_present = false }
  40. 8 if value_present
  41. 2 type = additional_types.fetch(name, types[name])
  42. 2 @casted_values[name] = type.deserialize(value)
  43. else
  44. 6 attr = default_attribute(name, value_present, value)
  45. 6 attr.value(&block)
  46. end
  47. end
  48. end
  49. 1 protected
  50. 1 def attributes
  51. 10 unless @materialized
  52. 23 values.each_key { |key| self[key] }
  53. 23 types.each_key { |key| self[key] }
  54. 8 @materialized = true
  55. end
  56. 10 @attributes
  57. end
  58. 1 private
  59. 1 attr_reader :values, :types, :additional_types, :default_attributes
  60. 1 def default_attribute(
  61. name,
  62. value_present = true,
  63. 7 value = values.fetch(name) { value_present = false }
  64. )
  65. 43 type = additional_types.fetch(name, types[name])
  66. 43 if value_present
  67. 30 @attributes[name] = Attribute.from_database(name, value, type, @casted_values[name])
  68. 13 elsif types.key?(name)
  69. 9 if attr = default_attributes[name]
  70. 1 @attributes[name] = attr.dup
  71. else
  72. 8 @attributes[name] = Attribute.uninitialized(name, type)
  73. end
  74. else
  75. 4 Attribute.null(name)
  76. end
  77. end
  78. end
  79. 1 class LazyAttributeHash # :nodoc:
  80. 1 delegate :transform_values, :each_value, :fetch, :except, to: :materialize
  81. 1 def initialize(types, values, additional_types, default_attributes, delegate_hash = {})
  82. 2 @types = types
  83. 2 @values = values
  84. 2 @additional_types = additional_types
  85. 2 @materialized = false
  86. 2 @delegate_hash = delegate_hash
  87. 2 @default_attributes = default_attributes
  88. end
  89. 1 def key?(key)
  90. delegate_hash.key?(key) || values.key?(key) || types.key?(key)
  91. end
  92. 1 def [](key)
  93. 4 delegate_hash[key] || assign_default_value(key)
  94. end
  95. 1 def []=(key, value)
  96. delegate_hash[key] = value
  97. end
  98. 1 def deep_dup
  99. dup.tap do |copy|
  100. copy.instance_variable_set(:@delegate_hash, delegate_hash.transform_values(&:dup))
  101. end
  102. end
  103. 1 def initialize_dup(_)
  104. @delegate_hash = Hash[delegate_hash]
  105. super
  106. end
  107. 1 def each_key(&block)
  108. 1 keys = types.keys | values.keys | delegate_hash.keys
  109. 1 keys.each(&block)
  110. end
  111. 1 def ==(other)
  112. if other.is_a?(LazyAttributeHash)
  113. materialize == other.materialize
  114. else
  115. materialize == other
  116. end
  117. end
  118. 1 def marshal_dump
  119. [@types, @values, @additional_types, @default_attributes, @delegate_hash]
  120. end
  121. 1 def marshal_load(values)
  122. 1 if values.is_a?(Hash)
  123. 1 ActiveSupport::Deprecation.warn(<<~MSG)
  124. Marshalling load from legacy attributes format is deprecated and will be removed in Rails 6.2.
  125. MSG
  126. 1 empty_hash = {}.freeze
  127. 1 initialize(empty_hash, empty_hash, empty_hash, empty_hash, values)
  128. 1 @materialized = true
  129. else
  130. initialize(*values)
  131. end
  132. end
  133. 1 protected
  134. 1 def materialize
  135. 1 unless @materialized
  136. 2 values.each_key { |key| self[key] }
  137. 2 types.each_key { |key| self[key] }
  138. 1 unless frozen?
  139. 1 @materialized = true
  140. end
  141. end
  142. 1 delegate_hash
  143. end
  144. 1 private
  145. 1 attr_reader :types, :values, :additional_types, :delegate_hash, :default_attributes
  146. 1 def assign_default_value(name)
  147. 1 type = additional_types.fetch(name, types[name])
  148. 1 value_present = true
  149. 1 value = values.fetch(name) { value_present = false }
  150. 1 if value_present
  151. 1 delegate_hash[name] = Attribute.from_database(name, value, type)
  152. elsif types.key?(name)
  153. attr = default_attributes[name]
  154. if attr
  155. delegate_hash[name] = attr.dup
  156. else
  157. delegate_hash[name] = Attribute.uninitialized(name, type)
  158. end
  159. end
  160. end
  161. end
  162. end

lib/active_model/attribute_set/yaml_encoder.rb

40.0% lines covered

20 relevant lines. 8 lines covered and 12 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 class AttributeSet
  4. # Attempts to do more intelligent YAML dumping of an
  5. # ActiveModel::AttributeSet to reduce the size of the resulting string
  6. 1 class YAMLEncoder # :nodoc:
  7. 1 def initialize(default_types)
  8. @default_types = default_types
  9. end
  10. 1 def encode(attribute_set, coder)
  11. coder["concise_attributes"] = attribute_set.each_value.map do |attr|
  12. if attr.type.equal?(default_types[attr.name])
  13. attr.with_type(nil)
  14. else
  15. attr
  16. end
  17. end
  18. end
  19. 1 def decode(coder)
  20. if coder["attributes"]
  21. coder["attributes"]
  22. else
  23. attributes_hash = Hash[coder["concise_attributes"].map do |attr|
  24. if attr.type.nil?
  25. attr = attr.with_type(default_types[attr.name])
  26. end
  27. [attr.name, attr]
  28. end]
  29. AttributeSet.new(attributes_hash)
  30. end
  31. end
  32. 1 private
  33. 1 attr_reader :default_types
  34. end
  35. end
  36. end

lib/active_model/attributes.rb

90.16% lines covered

61 relevant lines. 55 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/attribute_set"
  3. 1 require "active_model/attribute/user_provided_default"
  4. 1 module ActiveModel
  5. 1 module Attributes #:nodoc:
  6. 1 extend ActiveSupport::Concern
  7. 1 include ActiveModel::AttributeMethods
  8. 1 included do
  9. 2 attribute_method_suffix "="
  10. 2 class_attribute :attribute_types, :_default_attributes, instance_accessor: false
  11. 2 self.attribute_types = Hash.new(Type.default_value)
  12. 2 self._default_attributes = AttributeSet.new({})
  13. end
  14. 1 module ClassMethods
  15. 1 def attribute(name, type = Type::Value.new, **options)
  16. 10 name = name.to_s
  17. 10 if type.is_a?(Symbol)
  18. 10 type = ActiveModel::Type.lookup(type, **options.except(:default))
  19. end
  20. 10 self.attribute_types = attribute_types.merge(name => type)
  21. 10 define_default_attribute(name, options.fetch(:default, NO_DEFAULT_PROVIDED), type)
  22. 10 define_attribute_method(name)
  23. end
  24. # Returns an array of attribute names as strings
  25. #
  26. # class Person
  27. # include ActiveModel::Attributes
  28. #
  29. # attribute :name, :string
  30. # attribute :age, :integer
  31. # end
  32. #
  33. # Person.attribute_names
  34. # # => ["name", "age"]
  35. 1 def attribute_names
  36. 1 attribute_types.keys
  37. end
  38. 1 private
  39. 1 def define_method_attribute=(name, owner:)
  40. ActiveModel::AttributeMethods::AttrNames.define_attribute_accessor_method(
  41. owner, name, writer: true,
  42. 10 ) do |temp_method_name, attr_name_expr|
  43. owner <<
  44. "def #{temp_method_name}(value)" <<
  45. 10 " _write_attribute(#{attr_name_expr}, value)" <<
  46. "end"
  47. end
  48. end
  49. 1 NO_DEFAULT_PROVIDED = Object.new # :nodoc:
  50. 1 private_constant :NO_DEFAULT_PROVIDED
  51. 1 def define_default_attribute(name, value, type)
  52. 10 self._default_attributes = _default_attributes.deep_dup
  53. 10 if value == NO_DEFAULT_PROVIDED
  54. 8 default_attribute = _default_attributes[name].with_type(type)
  55. else
  56. 2 default_attribute = Attribute::UserProvidedDefault.new(
  57. name,
  58. value,
  59. type,
  60. 2 _default_attributes.fetch(name.to_s) { nil },
  61. )
  62. end
  63. 10 _default_attributes[name] = default_attribute
  64. end
  65. end
  66. 1 def initialize(*)
  67. 29 @attributes = self.class._default_attributes.deep_dup
  68. 29 super
  69. end
  70. 1 def initialize_dup(other) # :nodoc:
  71. 1 @attributes = @attributes.deep_dup
  72. 1 super
  73. end
  74. # Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
  75. #
  76. # class Person
  77. # include ActiveModel::Attributes
  78. #
  79. # attribute :name, :string
  80. # attribute :age, :integer
  81. # end
  82. #
  83. # person = Person.new(name: 'Francesco', age: 22)
  84. # person.attributes
  85. # # => {"name"=>"Francesco", "age"=>22}
  86. 1 def attributes
  87. 5 @attributes.to_hash
  88. end
  89. # Returns an array of attribute names as strings
  90. #
  91. # class Person
  92. # include ActiveModel::Attributes
  93. #
  94. # attribute :name, :string
  95. # attribute :age, :integer
  96. # end
  97. #
  98. # person = Person.new
  99. # person.attribute_names
  100. # # => ["name", "age"]
  101. 1 def attribute_names
  102. 1 @attributes.keys
  103. end
  104. 1 def freeze
  105. 1 @attributes = @attributes.clone.freeze
  106. 1 super
  107. end
  108. 1 private
  109. 1 def write_attribute(attr_name, value)
  110. name = attr_name.to_s
  111. name = self.class.attribute_aliases[name] || name
  112. @attributes.write_from_user(name, value)
  113. end
  114. 1 def _write_attribute(attr_name, value)
  115. 55 @attributes.write_from_user(attr_name, value)
  116. end
  117. 1 alias :attribute= :_write_attribute
  118. 1 def read_attribute(attr_name)
  119. name = attr_name.to_s
  120. name = self.class.attribute_aliases[name] || name
  121. @attributes.fetch_value(name)
  122. end
  123. 1 def attribute(attr_name)
  124. 21 @attributes.fetch_value(attr_name)
  125. end
  126. end
  127. end

lib/active_model/callbacks.rb

100.0% lines covered

32 relevant lines. 32 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/array/extract_options"
  3. 1 require "active_support/core_ext/hash/keys"
  4. 1 module ActiveModel
  5. # == Active \Model \Callbacks
  6. #
  7. # Provides an interface for any class to have Active Record like callbacks.
  8. #
  9. # Like the Active Record methods, the callback chain is aborted as soon as
  10. # one of the methods throws +:abort+.
  11. #
  12. # First, extend ActiveModel::Callbacks from the class you are creating:
  13. #
  14. # class MyModel
  15. # extend ActiveModel::Callbacks
  16. # end
  17. #
  18. # Then define a list of methods that you want callbacks attached to:
  19. #
  20. # define_model_callbacks :create, :update
  21. #
  22. # This will provide all three standard callbacks (before, around and after)
  23. # for both the <tt>:create</tt> and <tt>:update</tt> methods. To implement,
  24. # you need to wrap the methods you want callbacks on in a block so that the
  25. # callbacks get a chance to fire:
  26. #
  27. # def create
  28. # run_callbacks :create do
  29. # # Your create action methods here
  30. # end
  31. # end
  32. #
  33. # Then in your class, you can use the +before_create+, +after_create+ and
  34. # +around_create+ methods, just as you would in an Active Record model.
  35. #
  36. # before_create :action_before_create
  37. #
  38. # def action_before_create
  39. # # Your code here
  40. # end
  41. #
  42. # When defining an around callback remember to yield to the block, otherwise
  43. # it won't be executed:
  44. #
  45. # around_create :log_status
  46. #
  47. # def log_status
  48. # puts 'going to call the block...'
  49. # yield
  50. # puts 'block successfully called.'
  51. # end
  52. #
  53. # You can choose to have only specific callbacks by passing a hash to the
  54. # +define_model_callbacks+ method.
  55. #
  56. # define_model_callbacks :create, only: [:after, :before]
  57. #
  58. # Would only create the +after_create+ and +before_create+ callback methods in
  59. # your class.
  60. #
  61. # NOTE: Calling the same callback multiple times will overwrite previous callback definitions.
  62. #
  63. 1 module Callbacks
  64. 1 def self.extended(base) #:nodoc:
  65. 18 base.class_eval do
  66. 18 include ActiveSupport::Callbacks
  67. end
  68. end
  69. # define_model_callbacks accepts the same options +define_callbacks+ does,
  70. # in case you want to overwrite a default. Besides that, it also accepts an
  71. # <tt>:only</tt> option, where you can choose if you want all types (before,
  72. # around or after) or just some.
  73. #
  74. # define_model_callbacks :initializer, only: :after
  75. #
  76. # Note, the <tt>only: <type></tt> hash will apply to all callbacks defined
  77. # on that method call. To get around this you can call the define_model_callbacks
  78. # method as many times as you need.
  79. #
  80. # define_model_callbacks :create, only: :after
  81. # define_model_callbacks :update, only: :before
  82. # define_model_callbacks :destroy, only: :around
  83. #
  84. # Would create +after_create+, +before_update+ and +around_destroy+ methods
  85. # only.
  86. #
  87. # You can pass in a class to before_<type>, after_<type> and around_<type>,
  88. # in which case the callback will call that class's <action>_<type> method
  89. # passing the object that the callback is being called on.
  90. #
  91. # class MyModel
  92. # extend ActiveModel::Callbacks
  93. # define_model_callbacks :create
  94. #
  95. # before_create AnotherClass
  96. # end
  97. #
  98. # class AnotherClass
  99. # def self.before_create( obj )
  100. # # obj is the MyModel instance that the callback is being called on
  101. # end
  102. # end
  103. #
  104. # NOTE: +method_name+ passed to define_model_callbacks must not end with
  105. # <tt>!</tt>, <tt>?</tt> or <tt>=</tt>.
  106. 1 def define_model_callbacks(*callbacks)
  107. 7 options = callbacks.extract_options!
  108. 7 options = {
  109. skip_after_callbacks_if_terminated: true,
  110. scope: [:kind, :name],
  111. only: [:before, :around, :after]
  112. }.merge!(options)
  113. 7 types = Array(options.delete(:only))
  114. 7 callbacks.each do |callback|
  115. 7 define_callbacks(callback, options)
  116. 7 types.each do |type|
  117. 15 send("_define_#{type}_model_callback", self, callback)
  118. end
  119. end
  120. end
  121. 1 private
  122. 1 def _define_before_model_callback(klass, callback)
  123. 5 klass.define_singleton_method("before_#{callback}") do |*args, **options, &block|
  124. 1 options.assert_valid_keys(:if, :unless, :prepend)
  125. 1 set_callback(:"#{callback}", :before, *args, options, &block)
  126. end
  127. end
  128. 1 def _define_around_model_callback(klass, callback)
  129. 5 klass.define_singleton_method("around_#{callback}") do |*args, **options, &block|
  130. 1 options.assert_valid_keys(:if, :unless, :prepend)
  131. 1 set_callback(:"#{callback}", :around, *args, options, &block)
  132. end
  133. end
  134. 1 def _define_after_model_callback(klass, callback)
  135. 5 klass.define_singleton_method("after_#{callback}") do |*args, **options, &block|
  136. 5 options.assert_valid_keys(:if, :unless, :prepend)
  137. 5 options[:prepend] = true
  138. 5 conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
  139. 12 v != false
  140. }
  141. 5 options[:if] = Array(options[:if]) << conditional
  142. 5 set_callback(:"#{callback}", :after, *args, options, &block)
  143. end
  144. end
  145. end
  146. end

lib/active_model/conversion.rb

100.0% lines covered

18 relevant lines. 18 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. # == Active \Model \Conversion
  4. #
  5. # Handles default conversions: to_model, to_key, to_param, and to_partial_path.
  6. #
  7. # Let's take for example this non-persisted object.
  8. #
  9. # class ContactMessage
  10. # include ActiveModel::Conversion
  11. #
  12. # # ContactMessage are never persisted in the DB
  13. # def persisted?
  14. # false
  15. # end
  16. # end
  17. #
  18. # cm = ContactMessage.new
  19. # cm.to_model == cm # => true
  20. # cm.to_key # => nil
  21. # cm.to_param # => nil
  22. # cm.to_partial_path # => "contact_messages/contact_message"
  23. 1 module Conversion
  24. 1 extend ActiveSupport::Concern
  25. # If your object is already designed to implement all of the \Active \Model
  26. # you can use the default <tt>:to_model</tt> implementation, which simply
  27. # returns +self+.
  28. #
  29. # class Person
  30. # include ActiveModel::Conversion
  31. # end
  32. #
  33. # person = Person.new
  34. # person.to_model == person # => true
  35. #
  36. # If your model does not act like an \Active \Model object, then you should
  37. # define <tt>:to_model</tt> yourself returning a proxy object that wraps
  38. # your object with \Active \Model compliant methods.
  39. 1 def to_model
  40. 42 self
  41. end
  42. # Returns an Array of all key attributes if any of the attributes is set, whether or not
  43. # the object is persisted. Returns +nil+ if there are no key attributes.
  44. #
  45. # class Person
  46. # include ActiveModel::Conversion
  47. # attr_accessor :id
  48. #
  49. # def initialize(id)
  50. # @id = id
  51. # end
  52. # end
  53. #
  54. # person = Person.new(1)
  55. # person.to_key # => [1]
  56. 1 def to_key
  57. 7 key = respond_to?(:id) && id
  58. 7 key ? [key] : nil
  59. end
  60. # Returns a +string+ representing the object's key suitable for use in URLs,
  61. # or +nil+ if <tt>persisted?</tt> is +false+.
  62. #
  63. # class Person
  64. # include ActiveModel::Conversion
  65. # attr_accessor :id
  66. #
  67. # def initialize(id)
  68. # @id = id
  69. # end
  70. #
  71. # def persisted?
  72. # true
  73. # end
  74. # end
  75. #
  76. # person = Person.new(1)
  77. # person.to_param # => "1"
  78. 1 def to_param
  79. 6 (persisted? && key = to_key) ? key.join("-") : nil
  80. end
  81. # Returns a +string+ identifying the path associated with the object.
  82. # ActionPack uses this to find a suitable partial to represent the object.
  83. #
  84. # class Person
  85. # include ActiveModel::Conversion
  86. # end
  87. #
  88. # person = Person.new
  89. # person.to_partial_path # => "people/person"
  90. 1 def to_partial_path
  91. 5 self.class._to_partial_path
  92. end
  93. 1 module ClassMethods #:nodoc:
  94. # Provide a class level cache for #to_partial_path. This is an
  95. # internal method and should not be accessed directly.
  96. 1 def _to_partial_path #:nodoc:
  97. 5 @_to_partial_path ||= begin
  98. 5 element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
  99. 5 collection = ActiveSupport::Inflector.tableize(name)
  100. 5 "#{collection}/#{element}"
  101. end
  102. end
  103. end
  104. end
  105. end

lib/active_model/dirty.rb

91.78% lines covered

73 relevant lines. 67 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/attribute_mutation_tracker"
  3. 1 module ActiveModel
  4. # == Active \Model \Dirty
  5. #
  6. # Provides a way to track changes in your object in the same way as
  7. # Active Record does.
  8. #
  9. # The requirements for implementing ActiveModel::Dirty are:
  10. #
  11. # * <tt>include ActiveModel::Dirty</tt> in your object.
  12. # * Call <tt>define_attribute_methods</tt> passing each method you want to
  13. # track.
  14. # * Call <tt>[attr_name]_will_change!</tt> before each change to the tracked
  15. # attribute.
  16. # * Call <tt>changes_applied</tt> after the changes are persisted.
  17. # * Call <tt>clear_changes_information</tt> when you want to reset the changes
  18. # information.
  19. # * Call <tt>restore_attributes</tt> when you want to restore previous data.
  20. #
  21. # A minimal implementation could be:
  22. #
  23. # class Person
  24. # include ActiveModel::Dirty
  25. #
  26. # define_attribute_methods :name
  27. #
  28. # def initialize
  29. # @name = nil
  30. # end
  31. #
  32. # def name
  33. # @name
  34. # end
  35. #
  36. # def name=(val)
  37. # name_will_change! unless val == @name
  38. # @name = val
  39. # end
  40. #
  41. # def save
  42. # # do persistence work
  43. #
  44. # changes_applied
  45. # end
  46. #
  47. # def reload!
  48. # # get the values from the persistence layer
  49. #
  50. # clear_changes_information
  51. # end
  52. #
  53. # def rollback!
  54. # restore_attributes
  55. # end
  56. # end
  57. #
  58. # A newly instantiated +Person+ object is unchanged:
  59. #
  60. # person = Person.new
  61. # person.changed? # => false
  62. #
  63. # Change the name:
  64. #
  65. # person.name = 'Bob'
  66. # person.changed? # => true
  67. # person.name_changed? # => true
  68. # person.name_changed?(from: nil, to: "Bob") # => true
  69. # person.name_was # => nil
  70. # person.name_change # => [nil, "Bob"]
  71. # person.name = 'Bill'
  72. # person.name_change # => [nil, "Bill"]
  73. #
  74. # Save the changes:
  75. #
  76. # person.save
  77. # person.changed? # => false
  78. # person.name_changed? # => false
  79. #
  80. # Reset the changes:
  81. #
  82. # person.previous_changes # => {"name" => [nil, "Bill"]}
  83. # person.name_previously_changed? # => true
  84. # person.name_previously_changed?(from: nil, to: "Bill") # => true
  85. # person.name_previous_change # => [nil, "Bill"]
  86. # person.name_previously_was # => nil
  87. # person.reload!
  88. # person.previous_changes # => {}
  89. #
  90. # Rollback the changes:
  91. #
  92. # person.name = "Uncle Bob"
  93. # person.rollback!
  94. # person.name # => "Bill"
  95. # person.name_changed? # => false
  96. #
  97. # Assigning the same value leaves the attribute unchanged:
  98. #
  99. # person.name = 'Bill'
  100. # person.name_changed? # => false
  101. # person.name_change # => nil
  102. #
  103. # Which attributes have changed?
  104. #
  105. # person.name = 'Bob'
  106. # person.changed # => ["name"]
  107. # person.changes # => {"name" => ["Bill", "Bob"]}
  108. #
  109. # If an attribute is modified in-place then make use of
  110. # <tt>[attribute_name]_will_change!</tt> to mark that the attribute is changing.
  111. # Otherwise \Active \Model can't track changes to in-place attributes. Note
  112. # that Active Record can detect in-place modifications automatically. You do
  113. # not need to call <tt>[attribute_name]_will_change!</tt> on Active Record models.
  114. #
  115. # person.name_will_change!
  116. # person.name_change # => ["Bill", "Bill"]
  117. # person.name << 'y'
  118. # person.name_change # => ["Bill", "Billy"]
  119. 1 module Dirty
  120. 1 extend ActiveSupport::Concern
  121. 1 include ActiveModel::AttributeMethods
  122. 1 included do
  123. 2 attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
  124. 2 attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
  125. 2 attribute_method_affix prefix: "restore_", suffix: "!"
  126. 2 attribute_method_affix prefix: "clear_", suffix: "_change"
  127. end
  128. 1 def initialize_dup(other) # :nodoc:
  129. 1 super
  130. 1 if self.class.respond_to?(:_default_attributes)
  131. @attributes = self.class._default_attributes.map do |attr|
  132. attr.with_value_from_user(@attributes.fetch_value(attr.name))
  133. end
  134. end
  135. 1 @mutations_from_database = nil
  136. end
  137. # Clears dirty data and moves +changes+ to +previous_changes+ and
  138. # +mutations_from_database+ to +mutations_before_last_save+ respectively.
  139. 1 def changes_applied
  140. 25 unless defined?(@attributes)
  141. 12 mutations_from_database.finalize_changes
  142. end
  143. 25 @mutations_before_last_save = mutations_from_database
  144. 25 forget_attribute_assignments
  145. 25 @mutations_from_database = nil
  146. end
  147. # Returns +true+ if any of the attributes has unsaved changes, +false+ otherwise.
  148. #
  149. # person.changed? # => false
  150. # person.name = 'bob'
  151. # person.changed? # => true
  152. 1 def changed?
  153. 18 mutations_from_database.any_changes?
  154. end
  155. # Returns an array with the name of the attributes with unsaved changes.
  156. #
  157. # person.changed # => []
  158. # person.name = 'bob'
  159. # person.changed # => ["name"]
  160. 1 def changed
  161. 6 mutations_from_database.changed_attribute_names
  162. end
  163. # Dispatch target for <tt>*_changed?</tt> attribute methods.
  164. 1 def attribute_changed?(attr_name, **options) # :nodoc:
  165. 48 mutations_from_database.changed?(attr_name.to_s, **options)
  166. end
  167. # Dispatch target for <tt>*_was</tt> attribute methods.
  168. 1 def attribute_was(attr_name) # :nodoc:
  169. 10 mutations_from_database.original_value(attr_name.to_s)
  170. end
  171. # Dispatch target for <tt>*_previously_changed?</tt> attribute methods.
  172. 1 def attribute_previously_changed?(attr_name, **options) # :nodoc:
  173. 8 mutations_before_last_save.changed?(attr_name.to_s, **options)
  174. end
  175. # Dispatch target for <tt>*_previously_was</tt> attribute methods.
  176. 1 def attribute_previously_was(attr_name) # :nodoc:
  177. mutations_before_last_save.original_value(attr_name.to_s)
  178. end
  179. # Restore all previous data of the provided attributes.
  180. 1 def restore_attributes(attr_names = changed)
  181. 10 attr_names.each { |attr_name| restore_attribute!(attr_name) }
  182. end
  183. # Clears all dirty data: current changes and previous changes.
  184. 1 def clear_changes_information
  185. 2 @mutations_before_last_save = nil
  186. 2 forget_attribute_assignments
  187. 2 @mutations_from_database = nil
  188. end
  189. 1 def clear_attribute_changes(attr_names)
  190. attr_names.each do |attr_name|
  191. clear_attribute_change(attr_name)
  192. end
  193. end
  194. # Returns a hash of the attributes with unsaved changes indicating their original
  195. # values like <tt>attr => original value</tt>.
  196. #
  197. # person.name # => "bob"
  198. # person.name = 'robert'
  199. # person.changed_attributes # => {"name" => "bob"}
  200. 1 def changed_attributes
  201. 10 mutations_from_database.changed_values
  202. end
  203. # Returns a hash of changed attributes indicating their original
  204. # and new values like <tt>attr => [original value, new value]</tt>.
  205. #
  206. # person.changes # => {}
  207. # person.name = 'bob'
  208. # person.changes # => { "name" => ["bill", "bob"] }
  209. 1 def changes
  210. 9 mutations_from_database.changes
  211. end
  212. # Returns a hash of attributes that were changed before the model was saved.
  213. #
  214. # person.name # => "bob"
  215. # person.name = 'robert'
  216. # person.save
  217. # person.previous_changes # => {"name" => ["bob", "robert"]}
  218. 1 def previous_changes
  219. 8 mutations_before_last_save.changes
  220. end
  221. 1 def attribute_changed_in_place?(attr_name) # :nodoc:
  222. mutations_from_database.changed_in_place?(attr_name.to_s)
  223. end
  224. 1 private
  225. 1 def clear_attribute_change(attr_name)
  226. 8 mutations_from_database.forget_change(attr_name.to_s)
  227. end
  228. 1 def mutations_from_database
  229. 193 @mutations_from_database ||= if defined?(@attributes)
  230. 31 ActiveModel::AttributeMutationTracker.new(@attributes)
  231. else
  232. 30 ActiveModel::ForcedMutationTracker.new(self)
  233. end
  234. end
  235. 1 def forget_attribute_assignments
  236. 27 @attributes = @attributes.map(&:forgetting_assignment) if defined?(@attributes)
  237. end
  238. 1 def mutations_before_last_save
  239. 18 @mutations_before_last_save ||= ActiveModel::NullMutationTracker.instance
  240. end
  241. # Dispatch target for <tt>*_change</tt> attribute methods.
  242. 1 def attribute_change(attr_name)
  243. 3 mutations_from_database.change_to_attribute(attr_name.to_s)
  244. end
  245. # Dispatch target for <tt>*_previous_change</tt> attribute methods.
  246. 1 def attribute_previous_change(attr_name)
  247. 2 mutations_before_last_save.change_to_attribute(attr_name.to_s)
  248. end
  249. # Dispatch target for <tt>*_will_change!</tt> attribute methods.
  250. 1 def attribute_will_change!(attr_name)
  251. 44 mutations_from_database.force_change(attr_name.to_s)
  252. end
  253. # Dispatch target for <tt>restore_*!</tt> attribute methods.
  254. 1 def restore_attribute!(attr_name)
  255. 8 attr_name = attr_name.to_s
  256. 8 if attribute_changed?(attr_name)
  257. 8 __send__("#{attr_name}=", attribute_was(attr_name))
  258. 8 clear_attribute_change(attr_name)
  259. end
  260. end
  261. end
  262. end

lib/active_model/error.rb

97.8% lines covered

91 relevant lines. 89 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/class/attribute"
  3. 1 module ActiveModel
  4. # == Active \Model \Error
  5. #
  6. # Represents one single error
  7. 1 class Error
  8. 1 CALLBACKS_OPTIONS = [:if, :unless, :on, :allow_nil, :allow_blank, :strict]
  9. 1 MESSAGE_OPTIONS = [:message]
  10. 1 class_attribute :i18n_customize_full_message, default: false
  11. 1 def self.full_message(attribute, message, base_class) # :nodoc:
  12. 60 return message if attribute == :base
  13. 56 attribute = attribute.to_s
  14. 56 if i18n_customize_full_message && base_class.respond_to?(:i18n_scope)
  15. 18 attribute = attribute.remove(/\[\d+\]/)
  16. 18 parts = attribute.split(".")
  17. 18 attribute_name = parts.pop
  18. 18 namespace = parts.join("/") unless parts.empty?
  19. 18 attributes_scope = "#{base_class.i18n_scope}.errors.models"
  20. 18 if namespace
  21. 11 defaults = base_class.lookup_ancestors.map do |klass|
  22. 22 [
  23. :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.attributes.#{attribute_name}.format",
  24. :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.format",
  25. ]
  26. end
  27. else
  28. 7 defaults = base_class.lookup_ancestors.map do |klass|
  29. 14 [
  30. :"#{attributes_scope}.#{klass.model_name.i18n_key}.attributes.#{attribute_name}.format",
  31. :"#{attributes_scope}.#{klass.model_name.i18n_key}.format",
  32. ]
  33. end
  34. end
  35. 18 defaults.flatten!
  36. else
  37. 38 defaults = []
  38. end
  39. 56 defaults << :"errors.format"
  40. 56 defaults << "%{attribute} %{message}"
  41. 56 attr_name = attribute.tr(".", "_").humanize
  42. 56 attr_name = base_class.human_attribute_name(attribute, default: attr_name)
  43. 56 I18n.t(defaults.shift,
  44. default: defaults,
  45. attribute: attr_name,
  46. message: message)
  47. end
  48. 1 def self.generate_message(attribute, type, base, options) # :nodoc:
  49. 559 type = options.delete(:message) if options[:message].is_a?(Symbol)
  50. 559 value = (attribute != :base ? base.send(:read_attribute_for_validation, attribute) : nil)
  51. 559 options = {
  52. model: base.model_name.human,
  53. attribute: base.class.human_attribute_name(attribute),
  54. value: value,
  55. object: base
  56. }.merge!(options)
  57. 559 if base.class.respond_to?(:i18n_scope)
  58. 513 i18n_scope = base.class.i18n_scope.to_s
  59. 513 attribute = attribute.to_s.remove(/\[\d+\]/)
  60. 513 defaults = base.class.lookup_ancestors.flat_map do |klass|
  61. 564 [ :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.attributes.#{attribute}.#{type}",
  62. :"#{i18n_scope}.errors.models.#{klass.model_name.i18n_key}.#{type}" ]
  63. end
  64. 513 defaults << :"#{i18n_scope}.errors.messages.#{type}"
  65. catch(:exception) do
  66. 433 translation = I18n.translate(defaults.first, **options.merge(default: defaults.drop(1), throw: true))
  67. 29 return translation unless translation.nil?
  68. 513 end unless options[:message]
  69. else
  70. 46 defaults = []
  71. end
  72. 530 defaults << :"errors.attributes.#{attribute}.#{type}"
  73. 530 defaults << :"errors.messages.#{type}"
  74. 530 key = defaults.shift
  75. 530 defaults = options.delete(:message) if options[:message]
  76. 530 options[:default] = defaults
  77. 530 I18n.translate(key, **options)
  78. end
  79. 1 def initialize(base, attribute, type = :invalid, **options)
  80. 704 @base = base
  81. 704 @attribute = attribute
  82. 704 @raw_type = type
  83. 704 @type = type || :invalid
  84. 704 @options = options
  85. end
  86. 1 def initialize_dup(other) # :nodoc:
  87. 4 @attribute = @attribute.dup
  88. 4 @raw_type = @raw_type.dup
  89. 4 @type = @type.dup
  90. 4 @options = @options.deep_dup
  91. end
  92. # The object which the error belongs to
  93. 1 attr_reader :base
  94. # The attribute of +base+ which the error belongs to
  95. 1 attr_reader :attribute
  96. # The type of error, defaults to `:invalid` unless specified
  97. 1 attr_reader :type
  98. # The raw value provided as the second parameter when calling `errors#add`
  99. 1 attr_reader :raw_type
  100. # The options provided when calling `errors#add`
  101. 1 attr_reader :options
  102. # Returns the error message.
  103. #
  104. # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
  105. # error.message
  106. # # => "is too short (minimum is 5 characters)"
  107. 1 def message
  108. 705 case raw_type
  109. when Symbol
  110. 600 self.class.generate_message(attribute, raw_type, @base, options.except(*CALLBACKS_OPTIONS))
  111. else
  112. 105 raw_type
  113. end
  114. end
  115. # Returns the error detail.
  116. #
  117. # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
  118. # error.detail
  119. # # => { error: :too_short, count: 5 }
  120. 1 def detail
  121. 20 { error: raw_type }.merge(options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS))
  122. end
  123. # Returns the full error message.
  124. #
  125. # error = ActiveModel::Error.new(person, :name, :too_short, count: 5)
  126. # error.full_message
  127. # # => "Name is too short (minimum is 5 characters)"
  128. 1 def full_message
  129. 37 self.class.full_message(attribute, message, @base.class)
  130. end
  131. # See if error matches provided +attribute+, +type+ and +options+.
  132. #
  133. # Omitted params are not checked for a match.
  134. 1 def match?(attribute, type = nil, **options)
  135. 650 if @attribute != attribute || (type && @type != type)
  136. 72 return false
  137. end
  138. 578 options.each do |key, value|
  139. 4 if @options[key] != value
  140. 2 return false
  141. end
  142. end
  143. 576 true
  144. end
  145. # See if error matches provided +attribute+, +type+ and +options+ exactly.
  146. #
  147. # All params must be equal to Error's own attributes to be considered a
  148. # strict match.
  149. 1 def strict_match?(attribute, type, **options)
  150. 18 return false unless match?(attribute, type)
  151. 15 options == @options.except(*CALLBACKS_OPTIONS + MESSAGE_OPTIONS)
  152. end
  153. 1 def ==(other) # :nodoc:
  154. 7 other.is_a?(self.class) && attributes_for_hash == other.attributes_for_hash
  155. end
  156. 1 alias eql? ==
  157. 1 def hash # :nodoc:
  158. attributes_for_hash.hash
  159. end
  160. 1 def inspect # :nodoc:
  161. "#<#{self.class.name} attribute=#{@attribute}, type=#{@type}, options=#{@options.inspect}>"
  162. end
  163. 1 protected
  164. 1 def attributes_for_hash
  165. 12 [@base, @attribute, @raw_type, @options.except(*CALLBACKS_OPTIONS)]
  166. end
  167. end
  168. end

lib/active_model/errors.rb

99.52% lines covered

207 relevant lines. 206 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/array/conversions"
  3. 1 require "active_support/core_ext/string/inflections"
  4. 1 require "active_support/core_ext/object/deep_dup"
  5. 1 require "active_support/core_ext/string/filters"
  6. 1 require "active_model/error"
  7. 1 require "active_model/nested_error"
  8. 1 require "forwardable"
  9. 1 module ActiveModel
  10. # == Active \Model \Errors
  11. #
  12. # Provides error related functionalities you can include in your object
  13. # for handling error messages and interacting with Action View helpers.
  14. #
  15. # A minimal implementation could be:
  16. #
  17. # class Person
  18. # # Required dependency for ActiveModel::Errors
  19. # extend ActiveModel::Naming
  20. #
  21. # def initialize
  22. # @errors = ActiveModel::Errors.new(self)
  23. # end
  24. #
  25. # attr_accessor :name
  26. # attr_reader :errors
  27. #
  28. # def validate!
  29. # errors.add(:name, :blank, message: "cannot be nil") if name.nil?
  30. # end
  31. #
  32. # # The following methods are needed to be minimally implemented
  33. #
  34. # def read_attribute_for_validation(attr)
  35. # send(attr)
  36. # end
  37. #
  38. # def self.human_attribute_name(attr, options = {})
  39. # attr
  40. # end
  41. #
  42. # def self.lookup_ancestors
  43. # [self]
  44. # end
  45. # end
  46. #
  47. # The last three methods are required in your object for +Errors+ to be
  48. # able to generate error messages correctly and also handle multiple
  49. # languages. Of course, if you extend your object with <tt>ActiveModel::Translation</tt>
  50. # you will not need to implement the last two. Likewise, using
  51. # <tt>ActiveModel::Validations</tt> will handle the validation related methods
  52. # for you.
  53. #
  54. # The above allows you to do:
  55. #
  56. # person = Person.new
  57. # person.validate! # => ["cannot be nil"]
  58. # person.errors.full_messages # => ["name cannot be nil"]
  59. # # etc..
  60. 1 class Errors
  61. 1 include Enumerable
  62. 1 extend Forwardable
  63. 1 def_delegators :@errors, :size, :clear, :blank?, :empty?, :uniq!, :any?
  64. # TODO: forward all enumerable methods after `each` deprecation is removed.
  65. 1 def_delegators :@errors, :count
  66. 1 LEGACY_ATTRIBUTES = [:messages, :details].freeze
  67. 1 private_constant :LEGACY_ATTRIBUTES
  68. # The actual array of +Error+ objects
  69. # This method is aliased to <tt>objects</tt>.
  70. 1 attr_reader :errors
  71. 1 alias :objects :errors
  72. # Pass in the instance of the object that is using the errors object.
  73. #
  74. # class Person
  75. # def initialize
  76. # @errors = ActiveModel::Errors.new(self)
  77. # end
  78. # end
  79. 1 def initialize(base)
  80. 650 @base = base
  81. 650 @errors = []
  82. end
  83. 1 def initialize_dup(other) # :nodoc:
  84. 2 @errors = other.errors.deep_dup
  85. 2 super
  86. end
  87. # Copies the errors from <tt>other</tt>.
  88. # For copying errors but keep <tt>@base</tt> as is.
  89. #
  90. # other - The ActiveModel::Errors instance.
  91. #
  92. # Examples
  93. #
  94. # person.errors.copy!(other)
  95. 1 def copy!(other) # :nodoc:
  96. 2 @errors = other.errors.deep_dup
  97. 2 @errors.each { |error|
  98. 2 error.instance_variable_set(:@base, @base)
  99. }
  100. end
  101. # Imports one error
  102. # Imported errors are wrapped as a NestedError,
  103. # providing access to original error object.
  104. # If attribute or type needs to be overridden, use `override_options`.
  105. #
  106. # override_options - Hash
  107. # @option override_options [Symbol] :attribute Override the attribute the error belongs to
  108. # @option override_options [Symbol] :type Override type of the error.
  109. 1 def import(error, override_options = {})
  110. 3 [:attribute, :type].each do |key|
  111. 6 if override_options.key?(key)
  112. 1 override_options[key] = override_options[key].to_sym
  113. end
  114. end
  115. 3 @errors.append(NestedError.new(@base, error, override_options))
  116. end
  117. # Merges the errors from <tt>other</tt>,
  118. # each <tt>Error</tt> wrapped as <tt>NestedError</tt>.
  119. #
  120. # other - The ActiveModel::Errors instance.
  121. #
  122. # Examples
  123. #
  124. # person.errors.merge!(other)
  125. 1 def merge!(other)
  126. 2 other.errors.each { |error|
  127. 2 import(error)
  128. }
  129. end
  130. # Removes all errors except the given keys. Returns a hash containing the removed errors.
  131. #
  132. # person.errors.keys # => [:name, :age, :gender, :city]
  133. # person.errors.slice!(:age, :gender) # => { :name=>["cannot be nil"], :city=>["cannot be nil"] }
  134. # person.errors.keys # => [:age, :gender]
  135. 1 def slice!(*keys)
  136. 2 deprecation_removal_warning(:slice!)
  137. 2 keys = keys.map(&:to_sym)
  138. 2 results = messages.dup.slice!(*keys)
  139. 2 @errors.keep_if do |error|
  140. 8 keys.include?(error.attribute)
  141. end
  142. 2 results
  143. end
  144. # Search for errors matching +attribute+, +type+ or +options+.
  145. #
  146. # Only supplied params will be matched.
  147. #
  148. # person.errors.where(:name) # => all name errors.
  149. # person.errors.where(:name, :too_short) # => all name errors being too short
  150. # person.errors.where(:name, :too_short, minimum: 2) # => all name errors being too short and minimum is 2
  151. 1 def where(attribute, type = nil, **options)
  152. 790 attribute, type, options = normalize_arguments(attribute, type, **options)
  153. 790 @errors.select { |error|
  154. 616 error.match?(attribute, type, **options)
  155. }
  156. end
  157. # Returns +true+ if the error messages include an error for the given key
  158. # +attribute+, +false+ otherwise.
  159. #
  160. # person.errors.messages # => {:name=>["cannot be nil"]}
  161. # person.errors.include?(:name) # => true
  162. # person.errors.include?(:age) # => false
  163. 1 def include?(attribute)
  164. 10 @errors.any? { |error|
  165. 6 error.match?(attribute.to_sym)
  166. }
  167. end
  168. 1 alias :has_key? :include?
  169. 1 alias :key? :include?
  170. # Delete messages for +key+. Returns the deleted messages.
  171. #
  172. # person.errors[:name] # => ["cannot be nil"]
  173. # person.errors.delete(:name) # => ["cannot be nil"]
  174. # person.errors[:name] # => []
  175. 1 def delete(attribute, type = nil, **options)
  176. 10 attribute, type, options = normalize_arguments(attribute, type, **options)
  177. 10 matches = where(attribute, type, **options)
  178. 10 matches.each do |error|
  179. 6 @errors.delete(error)
  180. end
  181. 10 matches.map(&:message).presence
  182. end
  183. # When passed a symbol or a name of a method, returns an array of errors
  184. # for the method.
  185. #
  186. # person.errors[:name] # => ["cannot be nil"]
  187. # person.errors['name'] # => ["cannot be nil"]
  188. 1 def [](attribute)
  189. 747 DeprecationHandlingMessageArray.new(messages_for(attribute), self, attribute)
  190. end
  191. # Iterates through each error object.
  192. #
  193. # person.errors.add(:name, :too_short, count: 2)
  194. # person.errors.each do |error|
  195. # # Will yield <#ActiveModel::Error attribute=name, type=too_short,
  196. # options={:count=>3}>
  197. # end
  198. #
  199. # To be backward compatible with past deprecated hash-like behavior,
  200. # when block accepts two parameters instead of one, it
  201. # iterates through each error key, value pair in the error messages hash.
  202. # Yields the attribute and the error for that attribute. If the attribute
  203. # has more than one error message, yields once for each error message.
  204. #
  205. # person.errors.add(:name, :blank, message: "can't be blank")
  206. # person.errors.each do |attribute, message|
  207. # # Will yield :name and "can't be blank"
  208. # end
  209. #
  210. # person.errors.add(:name, :not_specified, message: "must be specified")
  211. # person.errors.each do |attribute, message|
  212. # # Will yield :name and "can't be blank"
  213. # # then yield :name and "must be specified"
  214. # end
  215. 1 def each(&block)
  216. 4 if block.arity <= 1
  217. 3 @errors.each(&block)
  218. else
  219. 1 ActiveSupport::Deprecation.warn(<<~MSG)
  220. Enumerating ActiveModel::Errors as a hash has been deprecated.
  221. In Rails 6.1, `errors` is an array of Error objects,
  222. therefore it should be accessed by a block with a single block
  223. parameter like this:
  224. person.errors.each do |error|
  225. attribute = error.attribute
  226. message = error.message
  227. end
  228. You are passing a block expecting two parameters,
  229. so the old hash behavior is simulated. As this is deprecated,
  230. this will result in an ArgumentError in Rails 6.2.
  231. MSG
  232. @errors.
  233. 1 sort { |a, b| a.attribute <=> b.attribute }.
  234. 3 each { |error| yield error.attribute, error.message }
  235. end
  236. end
  237. # Returns all message values.
  238. #
  239. # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
  240. # person.errors.values # => [["cannot be nil", "must be specified"]]
  241. 1 def values
  242. 2 deprecation_removal_warning(:values, "errors.map { |error| error.message }")
  243. 2 @errors.map(&:message).freeze
  244. end
  245. # Returns all message keys.
  246. #
  247. # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
  248. # person.errors.keys # => [:name]
  249. 1 def keys
  250. 7 deprecation_removal_warning(:keys, "errors.attribute_names")
  251. 7 keys = @errors.map(&:attribute)
  252. 7 keys.uniq!
  253. 7 keys.freeze
  254. end
  255. # Returns all error attribute names
  256. #
  257. # person.errors.messages # => {:name=>["cannot be nil", "must be specified"]}
  258. # person.errors.attribute_names # => [:name]
  259. 1 def attribute_names
  260. 3 @errors.map(&:attribute).uniq.freeze
  261. end
  262. # Returns an xml formatted representation of the Errors hash.
  263. #
  264. # person.errors.add(:name, :blank, message: "can't be blank")
  265. # person.errors.add(:name, :not_specified, message: "must be specified")
  266. # person.errors.to_xml
  267. # # =>
  268. # # <?xml version=\"1.0\" encoding=\"UTF-8\"?>
  269. # # <errors>
  270. # # <error>name can't be blank</error>
  271. # # <error>name must be specified</error>
  272. # # </errors>
  273. 1 def to_xml(options = {})
  274. 1 deprecation_removal_warning(:to_xml)
  275. 1 to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
  276. end
  277. # Returns a Hash that can be used as the JSON representation for this
  278. # object. You can pass the <tt>:full_messages</tt> option. This determines
  279. # if the json object should contain full messages or not (false by default).
  280. #
  281. # person.errors.as_json # => {:name=>["cannot be nil"]}
  282. # person.errors.as_json(full_messages: true) # => {:name=>["name cannot be nil"]}
  283. 1 def as_json(options = nil)
  284. 5 to_hash(options && options[:full_messages])
  285. end
  286. # Returns a Hash of attributes with their error messages. If +full_messages+
  287. # is +true+, it will contain full messages (see +full_message+).
  288. #
  289. # person.errors.to_hash # => {:name=>["cannot be nil"]}
  290. # person.errors.to_hash(true) # => {:name=>["name cannot be nil"]}
  291. 1 def to_hash(full_messages = false)
  292. 114 hash = {}
  293. 114 message_method = full_messages ? :full_message : :message
  294. 114 group_by_attribute.each do |attribute, errors|
  295. 108 hash[attribute] = errors.map(&message_method)
  296. end
  297. 114 hash
  298. end
  299. 1 def to_h
  300. 1 ActiveSupport::Deprecation.warn(<<~EOM)
  301. ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2.
  302. Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
  303. returned by `ActiveModel::Errors.to_hash` is an array of error messages.
  304. EOM
  305. 2 to_hash.transform_values { |values| values.last }
  306. end
  307. # Returns a Hash of attributes with an array of their error messages.
  308. #
  309. # Updating this hash would still update errors state for backward
  310. # compatibility, but this behavior is deprecated.
  311. 1 def messages
  312. 102 DeprecationHandlingMessageHash.new(self)
  313. end
  314. # Returns a Hash of attributes with an array of their error details.
  315. #
  316. # Updating this hash would still update errors state for backward
  317. # compatibility, but this behavior is deprecated.
  318. 1 def details
  319. 20 hash = {}
  320. 20 group_by_attribute.each do |attribute, errors|
  321. 18 hash[attribute] = errors.map(&:detail)
  322. end
  323. 20 DeprecationHandlingDetailsHash.new(hash)
  324. end
  325. # Returns a Hash of attributes with an array of their Error objects.
  326. #
  327. # person.errors.group_by_attribute
  328. # # => {:name=>[<#ActiveModel::Error>, <#ActiveModel::Error>]}
  329. 1 def group_by_attribute
  330. 135 @errors.group_by(&:attribute)
  331. end
  332. # Adds a new error of +type+ on +attribute+.
  333. # More than one error can be added to the same +attribute+.
  334. # If no +type+ is supplied, <tt>:invalid</tt> is assumed.
  335. #
  336. # person.errors.add(:name)
  337. # # Adds <#ActiveModel::Error attribute=name, type=invalid>
  338. # person.errors.add(:name, :not_implemented, message: "must be implemented")
  339. # # Adds <#ActiveModel::Error attribute=name, type=not_implemented,
  340. # options={:message=>"must be implemented"}>
  341. #
  342. # person.errors.messages
  343. # # => {:name=>["is invalid", "must be implemented"]}
  344. #
  345. # If +type+ is a string, it will be used as error message.
  346. #
  347. # If +type+ is a symbol, it will be translated using the appropriate
  348. # scope (see +generate_message+).
  349. #
  350. # If +type+ is a proc, it will be called, allowing for things like
  351. # <tt>Time.now</tt> to be used within an error.
  352. #
  353. # If the <tt>:strict</tt> option is set to +true+, it will raise
  354. # ActiveModel::StrictValidationFailed instead of adding the error.
  355. # <tt>:strict</tt> option can also be set to any other exception.
  356. #
  357. # person.errors.add(:name, :invalid, strict: true)
  358. # # => ActiveModel::StrictValidationFailed: Name is invalid
  359. # person.errors.add(:name, :invalid, strict: NameIsInvalid)
  360. # # => NameIsInvalid: Name is invalid
  361. #
  362. # person.errors.messages # => {}
  363. #
  364. # +attribute+ should be set to <tt>:base</tt> if the error is not
  365. # directly associated with a single attribute.
  366. #
  367. # person.errors.add(:base, :name_or_email_blank,
  368. # message: "either name or email must be present")
  369. # person.errors.messages
  370. # # => {:base=>["either name or email must be present"]}
  371. # person.errors.details
  372. # # => {:base=>[{error: :name_or_email_blank}]}
  373. 1 def add(attribute, type = :invalid, **options)
  374. 669 attribute, type, options = normalize_arguments(attribute, type, **options)
  375. 669 error = Error.new(@base, attribute, type, **options)
  376. 669 if exception = options[:strict]
  377. 6 exception = ActiveModel::StrictValidationFailed if exception == true
  378. 6 raise exception, error.full_message
  379. end
  380. 663 @errors.append(error)
  381. 663 error
  382. end
  383. # Returns +true+ if an error matches provided +attribute+ and +type+,
  384. # or +false+ otherwise. +type+ is treated the same as for +add+.
  385. #
  386. # person.errors.add :name, :blank
  387. # person.errors.added? :name, :blank # => true
  388. # person.errors.added? :name, "can't be blank" # => true
  389. #
  390. # If the error requires options, then it returns +true+ with
  391. # the correct options, or +false+ with incorrect or missing options.
  392. #
  393. # person.errors.add :name, :too_long, { count: 25 }
  394. # person.errors.added? :name, :too_long, count: 25 # => true
  395. # person.errors.added? :name, "is too long (maximum is 25 characters)" # => true
  396. # person.errors.added? :name, :too_long, count: 24 # => false
  397. # person.errors.added? :name, :too_long # => false
  398. # person.errors.added? :name, "is too long" # => false
  399. 1 def added?(attribute, type = :invalid, options = {})
  400. 28 attribute, type, options = normalize_arguments(attribute, type, **options)
  401. 28 if type.is_a? Symbol
  402. 19 @errors.any? { |error|
  403. 18 error.strict_match?(attribute, type, **options)
  404. }
  405. else
  406. 9 messages_for(attribute).include?(type)
  407. end
  408. end
  409. # Returns +true+ if an error on the attribute with the given type is
  410. # present, or +false+ otherwise. +type+ is treated the same as for +add+.
  411. #
  412. # person.errors.add :age
  413. # person.errors.add :name, :too_long, { count: 25 }
  414. # person.errors.of_kind? :age # => true
  415. # person.errors.of_kind? :name # => false
  416. # person.errors.of_kind? :name, :too_long # => true
  417. # person.errors.of_kind? :name, "is too long (maximum is 25 characters)" # => true
  418. # person.errors.of_kind? :name, :not_too_long # => false
  419. # person.errors.of_kind? :name, "is too long" # => false
  420. 1 def of_kind?(attribute, type = :invalid)
  421. 16 attribute, type = normalize_arguments(attribute, type)
  422. 16 if type.is_a? Symbol
  423. 8 !where(attribute, type).empty?
  424. else
  425. 8 messages_for(attribute).include?(type)
  426. end
  427. end
  428. # Returns all the full error messages in an array.
  429. #
  430. # class Person
  431. # validates_presence_of :name, :address, :email
  432. # validates_length_of :name, in: 5..30
  433. # end
  434. #
  435. # person = Person.create(address: '123 First St.')
  436. # person.errors.full_messages
  437. # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank", "Email can't be blank"]
  438. 1 def full_messages
  439. 22 @errors.map(&:full_message)
  440. end
  441. 1 alias :to_a :full_messages
  442. # Returns all the full error messages for a given attribute in an array.
  443. #
  444. # class Person
  445. # validates_presence_of :name, :email
  446. # validates_length_of :name, in: 5..30
  447. # end
  448. #
  449. # person = Person.create()
  450. # person.errors.full_messages_for(:name)
  451. # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
  452. 1 def full_messages_for(attribute)
  453. 4 where(attribute).map(&:full_message).freeze
  454. end
  455. # Returns all the error messages for a given attribute in an array.
  456. #
  457. # class Person
  458. # validates_presence_of :name, :email
  459. # validates_length_of :name, in: 5..30
  460. # end
  461. #
  462. # person = Person.create()
  463. # person.errors.messages_for(:name)
  464. # # => ["is too short (minimum is 5 characters)", "can't be blank"]
  465. 1 def messages_for(attribute)
  466. 768 where(attribute).map(&:message)
  467. end
  468. # Returns a full message for a given attribute.
  469. #
  470. # person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
  471. 1 def full_message(attribute, message)
  472. 23 Error.full_message(attribute, message, @base.class)
  473. end
  474. # Translates an error message in its default scope
  475. # (<tt>activemodel.errors.messages</tt>).
  476. #
  477. # Error messages are first looked up in <tt>activemodel.errors.models.MODEL.attributes.ATTRIBUTE.MESSAGE</tt>,
  478. # if it's not there, it's looked up in <tt>activemodel.errors.models.MODEL.MESSAGE</tt> and if
  479. # that is not there also, it returns the translation of the default message
  480. # (e.g. <tt>activemodel.errors.messages.MESSAGE</tt>). The translated model
  481. # name, translated attribute name and the value are available for
  482. # interpolation.
  483. #
  484. # When using inheritance in your models, it will check all the inherited
  485. # models too, but only if the model itself hasn't been found. Say you have
  486. # <tt>class Admin < User; end</tt> and you wanted the translation for
  487. # the <tt>:blank</tt> error message for the <tt>title</tt> attribute,
  488. # it looks for these translations:
  489. #
  490. # * <tt>activemodel.errors.models.admin.attributes.title.blank</tt>
  491. # * <tt>activemodel.errors.models.admin.blank</tt>
  492. # * <tt>activemodel.errors.models.user.attributes.title.blank</tt>
  493. # * <tt>activemodel.errors.models.user.blank</tt>
  494. # * any default you provided through the +options+ hash (in the <tt>activemodel.errors</tt> scope)
  495. # * <tt>activemodel.errors.messages.blank</tt>
  496. # * <tt>errors.attributes.title.blank</tt>
  497. # * <tt>errors.messages.blank</tt>
  498. 1 def generate_message(attribute, type = :invalid, options = {})
  499. 34 Error.generate_message(attribute, type, @base, options)
  500. end
  501. 1 def marshal_load(array) # :nodoc:
  502. # Rails 5
  503. 2 @errors = []
  504. 2 @base = array[0]
  505. 2 add_from_legacy_details_hash(array[2])
  506. end
  507. 1 def init_with(coder) # :nodoc:
  508. 6 data = coder.map
  509. 6 data.each { |k, v|
  510. 14 next if LEGACY_ATTRIBUTES.include?(k.to_sym)
  511. 8 instance_variable_set(:"@#{k}", v)
  512. }
  513. 6 @errors ||= []
  514. # Legacy support Rails 5.x details hash
  515. 6 add_from_legacy_details_hash(data["details"]) if data.key?("details")
  516. end
  517. 1 private
  518. 1 def normalize_arguments(attribute, type, **options)
  519. # Evaluate proc first
  520. 1513 if type.respond_to?(:call)
  521. 7 type = type.call(@base, options)
  522. end
  523. 1513 [attribute.to_sym, type, options]
  524. end
  525. 1 def add_from_legacy_details_hash(details)
  526. 4 details.each { |attribute, errors|
  527. 2 errors.each { |error|
  528. 2 type = error.delete(:error)
  529. 2 add(attribute, type, **error)
  530. }
  531. }
  532. end
  533. 1 def deprecation_removal_warning(method_name, alternative_message = nil)
  534. 12 message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2."
  535. 12 if alternative_message
  536. 9 message << "\n\nTo achieve the same use:\n\n "
  537. 9 message << alternative_message
  538. end
  539. 12 ActiveSupport::Deprecation.warn(message)
  540. end
  541. 1 def deprecation_rename_warning(old_method_name, new_method_name)
  542. ActiveSupport::Deprecation.warn("ActiveModel::Errors##{old_method_name} is deprecated. Please call ##{new_method_name} instead.")
  543. end
  544. end
  545. 1 class DeprecationHandlingMessageHash < SimpleDelegator
  546. 1 def initialize(errors)
  547. 102 @errors = errors
  548. 102 super(prepare_content)
  549. end
  550. 1 def []=(attribute, value)
  551. 4 ActiveSupport::Deprecation.warn("Calling `[]=` to an ActiveModel::Errors is deprecated. Please call `ActiveModel::Errors#add` instead.")
  552. 4 @errors.delete(attribute)
  553. 4 Array(value).each do |message|
  554. 4 @errors.add(attribute, message)
  555. end
  556. 4 __setobj__ prepare_content
  557. end
  558. 1 def delete(attribute)
  559. 1 ActiveSupport::Deprecation.warn("Calling `delete` to an ActiveModel::Errors messages hash is deprecated. Please call `ActiveModel::Errors#delete` instead.")
  560. 1 @errors.delete(attribute)
  561. end
  562. 1 private
  563. 1 def prepare_content
  564. 106 content = @errors.to_hash
  565. 106 content.each do |attribute, value|
  566. 100 content[attribute] = DeprecationHandlingMessageArray.new(value, @errors, attribute)
  567. end
  568. 106 content.default_proc = proc do |hash, attribute|
  569. 8 hash = hash.dup
  570. 8 hash[attribute] = DeprecationHandlingMessageArray.new([], @errors, attribute)
  571. 8 __setobj__ hash.freeze
  572. 8 hash[attribute]
  573. end
  574. 106 content.freeze
  575. end
  576. end
  577. 1 class DeprecationHandlingMessageArray < SimpleDelegator
  578. 1 def initialize(content, errors, attribute)
  579. 855 @errors = errors
  580. 855 @attribute = attribute
  581. 855 super(content.freeze)
  582. end
  583. 1 def <<(message)
  584. 4 ActiveSupport::Deprecation.warn("Calling `<<` to an ActiveModel::Errors message array in order to add an error is deprecated. Please call `ActiveModel::Errors#add` instead.")
  585. 4 @errors.add(@attribute, message)
  586. 4 __setobj__ @errors.messages_for(@attribute)
  587. 4 self
  588. end
  589. 1 def clear
  590. 1 ActiveSupport::Deprecation.warn("Calling `clear` to an ActiveModel::Errors message array in order to delete all errors is deprecated. Please call `ActiveModel::Errors#delete` instead.")
  591. 1 @errors.delete(@attribute)
  592. end
  593. end
  594. 1 class DeprecationHandlingDetailsHash < SimpleDelegator
  595. 1 def initialize(details)
  596. 20 details.default = []
  597. 20 details.freeze
  598. 20 super(details)
  599. end
  600. end
  601. # Raised when a validation cannot be corrected by end users and are considered
  602. # exceptional.
  603. #
  604. # class Person
  605. # include ActiveModel::Validations
  606. #
  607. # attr_accessor :name
  608. #
  609. # validates_presence_of :name, strict: true
  610. # end
  611. #
  612. # person = Person.new
  613. # person.name = nil
  614. # person.valid?
  615. # # => ActiveModel::StrictValidationFailed: Name can't be blank
  616. 1 class StrictValidationFailed < StandardError
  617. end
  618. # Raised when attribute values are out of range.
  619. 1 class RangeError < ::RangeError
  620. end
  621. # Raised when unknown attributes are supplied via mass assignment.
  622. #
  623. # class Person
  624. # include ActiveModel::AttributeAssignment
  625. # include ActiveModel::Validations
  626. # end
  627. #
  628. # person = Person.new
  629. # person.assign_attributes(name: 'Gorby')
  630. # # => ActiveModel::UnknownAttributeError: unknown attribute 'name' for Person.
  631. 1 class UnknownAttributeError < NoMethodError
  632. 1 attr_reader :record, :attribute
  633. 1 def initialize(record, attribute)
  634. 4 @record = record
  635. 4 @attribute = attribute
  636. 4 super("unknown attribute '#{attribute}' for #{@record.class}.")
  637. end
  638. end
  639. end

lib/active_model/forbidden_attributes_protection.rb

100.0% lines covered

10 relevant lines. 10 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. # Raised when forbidden attributes are used for mass assignment.
  4. #
  5. # class Person < ActiveRecord::Base
  6. # end
  7. #
  8. # params = ActionController::Parameters.new(name: 'Bob')
  9. # Person.new(params)
  10. # # => ActiveModel::ForbiddenAttributesError
  11. #
  12. # params.permit!
  13. # Person.new(params)
  14. # # => #<Person id: nil, name: "Bob">
  15. 1 class ForbiddenAttributesError < StandardError
  16. end
  17. 1 module ForbiddenAttributesProtection # :nodoc:
  18. 1 private
  19. 1 def sanitize_for_mass_assignment(attributes)
  20. 22 if attributes.respond_to?(:permitted?)
  21. 4 raise ActiveModel::ForbiddenAttributesError if !attributes.permitted?
  22. 2 attributes.to_h
  23. else
  24. 18 attributes
  25. end
  26. end
  27. 1 alias :sanitize_forbidden_attributes :sanitize_for_mass_assignment
  28. end
  29. end

lib/active_model/gem_version.rb

88.89% lines covered

9 relevant lines. 8 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. # Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
  4. 1 def self.gem_version
  5. Gem::Version.new VERSION::STRING
  6. end
  7. 1 module VERSION
  8. 1 MAJOR = 6
  9. 1 MINOR = 1
  10. 1 TINY = 0
  11. 1 PRE = "alpha"
  12. 1 STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
  13. end
  14. end

lib/active_model/lint.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Lint
  4. # == Active \Model \Lint \Tests
  5. #
  6. # You can test whether an object is compliant with the Active \Model API by
  7. # including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will
  8. # include tests that tell you whether your object is fully compliant,
  9. # or if not, which aspects of the API are not implemented.
  10. #
  11. # Note an object is not required to implement all APIs in order to work
  12. # with Action Pack. This module only intends to provide guidance in case
  13. # you want all features out of the box.
  14. #
  15. # These tests do not attempt to determine the semantic correctness of the
  16. # returned values. For instance, you could implement <tt>valid?</tt> to
  17. # always return +true+, and the tests would pass. It is up to you to ensure
  18. # that the values are semantically meaningful.
  19. #
  20. # Objects you pass in are expected to return a compliant object from a call
  21. # to <tt>to_model</tt>. It is perfectly fine for <tt>to_model</tt> to return
  22. # +self+.
  23. 1 module Tests
  24. # Passes if the object's model responds to <tt>to_key</tt> and if calling
  25. # this method returns +nil+ when the object is not persisted.
  26. # Fails otherwise.
  27. #
  28. # <tt>to_key</tt> returns an Enumerable of all (primary) key attributes
  29. # of the model, and is used to a generate unique DOM id for the object.
  30. 1 def test_to_key
  31. 2 assert_respond_to model, :to_key
  32. 2 def model.persisted?() false end
  33. 2 assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
  34. end
  35. # Passes if the object's model responds to <tt>to_param</tt> and if
  36. # calling this method returns +nil+ when the object is not persisted.
  37. # Fails otherwise.
  38. #
  39. # <tt>to_param</tt> is used to represent the object's key in URLs.
  40. # Implementers can decide to either raise an exception or provide a
  41. # default in case the record uses a composite primary key. There are no
  42. # tests for this behavior in lint because it doesn't make sense to force
  43. # any of the possible implementation strategies on the implementer.
  44. 1 def test_to_param
  45. 2 assert_respond_to model, :to_param
  46. 2 def model.to_key() [1] end
  47. 4 def model.persisted?() false end
  48. 2 assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
  49. end
  50. # Passes if the object's model responds to <tt>to_partial_path</tt> and if
  51. # calling this method returns a string. Fails otherwise.
  52. #
  53. # <tt>to_partial_path</tt> is used for looking up partials. For example,
  54. # a BlogPost model might return "blog_posts/blog_post".
  55. 1 def test_to_partial_path
  56. 2 assert_respond_to model, :to_partial_path
  57. 2 assert_kind_of String, model.to_partial_path
  58. end
  59. # Passes if the object's model responds to <tt>persisted?</tt> and if
  60. # calling this method returns either +true+ or +false+. Fails otherwise.
  61. #
  62. # <tt>persisted?</tt> is used when calculating the URL for an object.
  63. # If the object is not persisted, a form for that object, for instance,
  64. # will route to the create action. If it is persisted, a form for the
  65. # object will route to the update action.
  66. 1 def test_persisted?
  67. 2 assert_respond_to model, :persisted?
  68. 2 assert_boolean model.persisted?, "persisted?"
  69. end
  70. # Passes if the object's model responds to <tt>model_name</tt> both as
  71. # an instance method and as a class method, and if calling this method
  72. # returns a string with some convenience methods: <tt>:human</tt>,
  73. # <tt>:singular</tt> and <tt>:plural</tt>.
  74. #
  75. # Check ActiveModel::Naming for more information.
  76. 1 def test_model_naming
  77. 2 assert_respond_to model.class, :model_name
  78. 2 model_name = model.class.model_name
  79. 2 assert_respond_to model_name, :to_str
  80. 2 assert_respond_to model_name.human, :to_str
  81. 2 assert_respond_to model_name.singular, :to_str
  82. 2 assert_respond_to model_name.plural, :to_str
  83. 2 assert_respond_to model, :model_name
  84. 2 assert_equal model.model_name, model.class.model_name
  85. end
  86. # Passes if the object's model responds to <tt>errors</tt> and if calling
  87. # <tt>[](attribute)</tt> on the result of this method returns an array.
  88. # Fails otherwise.
  89. #
  90. # <tt>errors[attribute]</tt> is used to retrieve the errors of a model
  91. # for a given attribute. If errors are present, the method should return
  92. # an array of strings that are the errors for the attribute in question.
  93. # If localization is used, the strings should be localized for the current
  94. # locale. If no error is present, the method should return an empty array.
  95. 1 def test_errors_aref
  96. 2 assert_respond_to model, :errors
  97. 2 assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
  98. end
  99. 1 private
  100. 1 def model
  101. 36 assert_respond_to @model, :to_model
  102. 36 @model.to_model
  103. end
  104. 1 def assert_boolean(result, name)
  105. 2 assert result == true || result == false, "#{name} should be a boolean"
  106. end
  107. end
  108. end
  109. end

lib/active_model/model.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. # == Active \Model \Basic \Model
  4. #
  5. # Includes the required interface for an object to interact with
  6. # Action Pack and Action View, using different Active Model modules.
  7. # It includes model name introspections, conversions, translations and
  8. # validations. Besides that, it allows you to initialize the object with a
  9. # hash of attributes, pretty much like Active Record does.
  10. #
  11. # A minimal implementation could be:
  12. #
  13. # class Person
  14. # include ActiveModel::Model
  15. # attr_accessor :name, :age
  16. # end
  17. #
  18. # person = Person.new(name: 'bob', age: '18')
  19. # person.name # => "bob"
  20. # person.age # => "18"
  21. #
  22. # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
  23. # to return +false+, which is the most common case. You may want to override
  24. # it in your class to simulate a different scenario:
  25. #
  26. # class Person
  27. # include ActiveModel::Model
  28. # attr_accessor :id, :name
  29. #
  30. # def persisted?
  31. # self.id == 1
  32. # end
  33. # end
  34. #
  35. # person = Person.new(id: 1, name: 'bob')
  36. # person.persisted? # => true
  37. #
  38. # Also, if for some reason you need to run code on <tt>initialize</tt>, make
  39. # sure you call +super+ if you want the attributes hash initialization to
  40. # happen.
  41. #
  42. # class Person
  43. # include ActiveModel::Model
  44. # attr_accessor :id, :name, :omg
  45. #
  46. # def initialize(attributes={})
  47. # super
  48. # @omg ||= true
  49. # end
  50. # end
  51. #
  52. # person = Person.new(id: 1, name: 'bob')
  53. # person.omg # => true
  54. #
  55. # For more detailed information on other functionalities available, please
  56. # refer to the specific modules included in <tt>ActiveModel::Model</tt>
  57. # (see below).
  58. 1 module Model
  59. 1 extend ActiveSupport::Concern
  60. 1 include ActiveModel::AttributeAssignment
  61. 1 include ActiveModel::Validations
  62. 1 include ActiveModel::Conversion
  63. 1 included do
  64. 5 extend ActiveModel::Naming
  65. 5 extend ActiveModel::Translation
  66. end
  67. # Initializes a new model with the given +params+.
  68. #
  69. # class Person
  70. # include ActiveModel::Model
  71. # attr_accessor :name, :age
  72. # end
  73. #
  74. # person = Person.new(name: 'bob', age: '18')
  75. # person.name # => "bob"
  76. # person.age # => "18"
  77. 1 def initialize(attributes = {})
  78. 52 assign_attributes(attributes) if attributes
  79. 50 super()
  80. end
  81. # Indicates if the model is persisted. Default is +false+.
  82. #
  83. # class Person
  84. # include ActiveModel::Model
  85. # attr_accessor :id, :name
  86. # end
  87. #
  88. # person = Person.new(id: 1, name: 'bob')
  89. # person.persisted? # => false
  90. 1 def persisted?
  91. 2 false
  92. end
  93. end
  94. end

lib/active_model/naming.rb

100.0% lines covered

61 relevant lines. 61 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/except"
  3. 1 require "active_support/core_ext/module/introspection"
  4. 1 require "active_support/core_ext/module/redefine_method"
  5. 1 module ActiveModel
  6. 1 class Name
  7. 1 include Comparable
  8. 1 attr_reader :singular, :plural, :element, :collection,
  9. :singular_route_key, :route_key, :param_key, :i18n_key,
  10. :name
  11. 1 alias_method :cache_key, :collection
  12. ##
  13. # :method: ==
  14. #
  15. # :call-seq:
  16. # ==(other)
  17. #
  18. # Equivalent to <tt>String#==</tt>. Returns +true+ if the class name and
  19. # +other+ are equal, otherwise +false+.
  20. #
  21. # class BlogPost
  22. # extend ActiveModel::Naming
  23. # end
  24. #
  25. # BlogPost.model_name == 'BlogPost' # => true
  26. # BlogPost.model_name == 'Blog Post' # => false
  27. ##
  28. # :method: ===
  29. #
  30. # :call-seq:
  31. # ===(other)
  32. #
  33. # Equivalent to <tt>#==</tt>.
  34. #
  35. # class BlogPost
  36. # extend ActiveModel::Naming
  37. # end
  38. #
  39. # BlogPost.model_name === 'BlogPost' # => true
  40. # BlogPost.model_name === 'Blog Post' # => false
  41. ##
  42. # :method: <=>
  43. #
  44. # :call-seq:
  45. # <=>(other)
  46. #
  47. # Equivalent to <tt>String#<=></tt>.
  48. #
  49. # class BlogPost
  50. # extend ActiveModel::Naming
  51. # end
  52. #
  53. # BlogPost.model_name <=> 'BlogPost' # => 0
  54. # BlogPost.model_name <=> 'Blog' # => 1
  55. # BlogPost.model_name <=> 'BlogPosts' # => -1
  56. ##
  57. # :method: =~
  58. #
  59. # :call-seq:
  60. # =~(regexp)
  61. #
  62. # Equivalent to <tt>String#=~</tt>. Match the class name against the given
  63. # regexp. Returns the position where the match starts or +nil+ if there is
  64. # no match.
  65. #
  66. # class BlogPost
  67. # extend ActiveModel::Naming
  68. # end
  69. #
  70. # BlogPost.model_name =~ /Post/ # => 4
  71. # BlogPost.model_name =~ /\d/ # => nil
  72. ##
  73. # :method: !~
  74. #
  75. # :call-seq:
  76. # !~(regexp)
  77. #
  78. # Equivalent to <tt>String#!~</tt>. Match the class name against the given
  79. # regexp. Returns +true+ if there is no match, otherwise +false+.
  80. #
  81. # class BlogPost
  82. # extend ActiveModel::Naming
  83. # end
  84. #
  85. # BlogPost.model_name !~ /Post/ # => false
  86. # BlogPost.model_name !~ /\d/ # => true
  87. ##
  88. # :method: eql?
  89. #
  90. # :call-seq:
  91. # eql?(other)
  92. #
  93. # Equivalent to <tt>String#eql?</tt>. Returns +true+ if the class name and
  94. # +other+ have the same length and content, otherwise +false+.
  95. #
  96. # class BlogPost
  97. # extend ActiveModel::Naming
  98. # end
  99. #
  100. # BlogPost.model_name.eql?('BlogPost') # => true
  101. # BlogPost.model_name.eql?('Blog Post') # => false
  102. ##
  103. # :method: match?
  104. #
  105. # :call-seq:
  106. # match?(regexp)
  107. #
  108. # Equivalent to <tt>String#match?</tt>. Match the class name against the
  109. # given regexp. Returns +true+ if there is a match, otherwise +false+.
  110. #
  111. # class BlogPost
  112. # extend ActiveModel::Naming
  113. # end
  114. #
  115. # BlogPost.model_name.match?(/Post/) # => true
  116. # BlogPost.model_name.match?(/\d/) # => false
  117. ##
  118. # :method: to_s
  119. #
  120. # :call-seq:
  121. # to_s()
  122. #
  123. # Returns the class name.
  124. #
  125. # class BlogPost
  126. # extend ActiveModel::Naming
  127. # end
  128. #
  129. # BlogPost.model_name.to_s # => "BlogPost"
  130. ##
  131. # :method: to_str
  132. #
  133. # :call-seq:
  134. # to_str()
  135. #
  136. # Equivalent to +to_s+.
  137. 1 delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
  138. :to_str, :as_json, to: :name
  139. # Returns a new ActiveModel::Name instance. By default, the +namespace+
  140. # and +name+ option will take the namespace and name of the given class
  141. # respectively.
  142. #
  143. # module Foo
  144. # class Bar
  145. # end
  146. # end
  147. #
  148. # ActiveModel::Name.new(Foo::Bar).to_s
  149. # # => "Foo::Bar"
  150. 1 def initialize(klass, namespace = nil, name = nil)
  151. 118 @name = name || klass.name
  152. 118 raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
  153. 117 @unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
  154. 117 @klass = klass
  155. 117 @singular = _singularize(@name)
  156. 117 @plural = ActiveSupport::Inflector.pluralize(@singular)
  157. 117 @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
  158. 117 @human = ActiveSupport::Inflector.humanize(@element)
  159. 117 @collection = ActiveSupport::Inflector.tableize(@name)
  160. 117 @param_key = (namespace ? _singularize(@unnamespaced) : @singular)
  161. 117 @i18n_key = @name.underscore.to_sym
  162. 117 @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
  163. 117 @singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
  164. 117 @route_key << "_index" if @plural == @singular
  165. end
  166. # Transform the model name into a more human format, using I18n. By default,
  167. # it will underscore then humanize the class name.
  168. #
  169. # class BlogPost
  170. # extend ActiveModel::Naming
  171. # end
  172. #
  173. # BlogPost.model_name.human # => "Blog post"
  174. #
  175. # Specify +options+ with additional translating options.
  176. 1 def human(options = {})
  177. 571 return @human unless @klass.respond_to?(:lookup_ancestors) &&
  178. @klass.respond_to?(:i18n_scope)
  179. 519 defaults = @klass.lookup_ancestors.map do |klass|
  180. 572 klass.model_name.i18n_key
  181. end
  182. 519 defaults << options[:default] if options[:default]
  183. 519 defaults << @human
  184. 519 options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
  185. 519 I18n.translate(defaults.shift, **options)
  186. end
  187. 1 private
  188. 1 def _singularize(string)
  189. 126 ActiveSupport::Inflector.underscore(string).tr("/", "_")
  190. end
  191. end
  192. # == Active \Model \Naming
  193. #
  194. # Creates a +model_name+ method on your object.
  195. #
  196. # To implement, just extend ActiveModel::Naming in your object:
  197. #
  198. # class BookCover
  199. # extend ActiveModel::Naming
  200. # end
  201. #
  202. # BookCover.model_name.name # => "BookCover"
  203. # BookCover.model_name.human # => "Book cover"
  204. #
  205. # BookCover.model_name.i18n_key # => :book_cover
  206. # BookModule::BookCover.model_name.i18n_key # => :"book_module/book_cover"
  207. #
  208. # Providing the functionality that ActiveModel::Naming provides in your object
  209. # is required to pass the \Active \Model Lint test. So either extending the
  210. # provided method below, or rolling your own is required.
  211. 1 module Naming
  212. 1 def self.extended(base) #:nodoc:
  213. 27 base.silence_redefinition_of_method :model_name
  214. 27 base.delegate :model_name, to: :class
  215. end
  216. # Returns an ActiveModel::Name object for module. It can be
  217. # used to retrieve all kinds of naming-related information
  218. # (See ActiveModel::Name for more information).
  219. #
  220. # class Person
  221. # extend ActiveModel::Naming
  222. # end
  223. #
  224. # Person.model_name.name # => "Person"
  225. # Person.model_name.class # => ActiveModel::Name
  226. # Person.model_name.singular # => "person"
  227. # Person.model_name.plural # => "people"
  228. 1 def model_name
  229. 3048 @_model_name ||= begin
  230. 84 namespace = module_parents.detect do |n|
  231. 158 n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
  232. end
  233. 84 ActiveModel::Name.new(self, namespace)
  234. end
  235. end
  236. # Returns the plural class name of a record or class.
  237. #
  238. # ActiveModel::Naming.plural(post) # => "posts"
  239. # ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people"
  240. 1 def self.plural(record_or_class)
  241. 5 model_name_from_record_or_class(record_or_class).plural
  242. end
  243. # Returns the singular class name of a record or class.
  244. #
  245. # ActiveModel::Naming.singular(post) # => "post"
  246. # ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person"
  247. 1 def self.singular(record_or_class)
  248. 4 model_name_from_record_or_class(record_or_class).singular
  249. end
  250. # Identifies whether the class name of a record or class is uncountable.
  251. #
  252. # ActiveModel::Naming.uncountable?(Sheep) # => true
  253. # ActiveModel::Naming.uncountable?(Post) # => false
  254. 1 def self.uncountable?(record_or_class)
  255. 2 plural(record_or_class) == singular(record_or_class)
  256. end
  257. # Returns string to use while generating route names. It differs for
  258. # namespaced models regarding whether it's inside isolated engine.
  259. #
  260. # # For isolated engine:
  261. # ActiveModel::Naming.singular_route_key(Blog::Post) # => "post"
  262. #
  263. # # For shared engine:
  264. # ActiveModel::Naming.singular_route_key(Blog::Post) # => "blog_post"
  265. 1 def self.singular_route_key(record_or_class)
  266. 3 model_name_from_record_or_class(record_or_class).singular_route_key
  267. end
  268. # Returns string to use while generating route names. It differs for
  269. # namespaced models regarding whether it's inside isolated engine.
  270. #
  271. # # For isolated engine:
  272. # ActiveModel::Naming.route_key(Blog::Post) # => "posts"
  273. #
  274. # # For shared engine:
  275. # ActiveModel::Naming.route_key(Blog::Post) # => "blog_posts"
  276. #
  277. # The route key also considers if the noun is uncountable and, in
  278. # such cases, automatically appends _index.
  279. 1 def self.route_key(record_or_class)
  280. 3 model_name_from_record_or_class(record_or_class).route_key
  281. end
  282. # Returns string to use for params names. It differs for
  283. # namespaced models regarding whether it's inside isolated engine.
  284. #
  285. # # For isolated engine:
  286. # ActiveModel::Naming.param_key(Blog::Post) # => "post"
  287. #
  288. # # For shared engine:
  289. # ActiveModel::Naming.param_key(Blog::Post) # => "blog_post"
  290. 1 def self.param_key(record_or_class)
  291. 2 model_name_from_record_or_class(record_or_class).param_key
  292. end
  293. 1 def self.model_name_from_record_or_class(record_or_class) #:nodoc:
  294. 17 if record_or_class.respond_to?(:to_model)
  295. 6 record_or_class.to_model.model_name
  296. else
  297. 11 record_or_class.model_name
  298. end
  299. end
  300. 1 private_class_method :model_name_from_record_or_class
  301. end
  302. end

lib/active_model/nested_error.rb

100.0% lines covered

14 relevant lines. 14 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/error"
  3. 1 require "forwardable"
  4. 1 module ActiveModel
  5. 1 class NestedError < Error
  6. 1 def initialize(base, inner_error, override_options = {})
  7. 7 @base = base
  8. 7 @inner_error = inner_error
  9. 12 @attribute = override_options.fetch(:attribute) { inner_error.attribute }
  10. 13 @type = override_options.fetch(:type) { inner_error.type }
  11. 7 @raw_type = inner_error.raw_type
  12. 7 @options = inner_error.options
  13. end
  14. 1 attr_reader :inner_error
  15. 1 extend Forwardable
  16. 1 def_delegators :@inner_error, :message
  17. end
  18. end

lib/active_model/railtie.rb

0.0% lines covered

14 relevant lines. 0 lines covered and 14 lines missed.
    
  1. # frozen_string_literal: true
  2. require "active_model"
  3. require "rails"
  4. module ActiveModel
  5. class Railtie < Rails::Railtie # :nodoc:
  6. config.eager_load_namespaces << ActiveModel
  7. config.active_model = ActiveSupport::OrderedOptions.new
  8. initializer "active_model.secure_password" do
  9. ActiveModel::SecurePassword.min_cost = Rails.env.test?
  10. end
  11. initializer "active_model.i18n_customize_full_message" do
  12. ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
  13. end
  14. end
  15. end

lib/active_model/secure_password.rb

94.44% lines covered

36 relevant lines. 34 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module SecurePassword
  4. 1 extend ActiveSupport::Concern
  5. # BCrypt hash function can handle maximum 72 bytes, and if we pass
  6. # password of length more than 72 bytes it ignores extra characters.
  7. # Hence need to put a restriction on password length.
  8. 1 MAX_PASSWORD_LENGTH_ALLOWED = 72
  9. 1 class << self
  10. 1 attr_accessor :min_cost # :nodoc:
  11. end
  12. 1 self.min_cost = false
  13. 1 module ClassMethods
  14. # Adds methods to set and authenticate against a BCrypt password.
  15. # This mechanism requires you to have a +XXX_digest+ attribute.
  16. # Where +XXX+ is the attribute name of your desired password.
  17. #
  18. # The following validations are added automatically:
  19. # * Password must be present on creation
  20. # * Password length should be less than or equal to 72 bytes
  21. # * Confirmation of password (using a +XXX_confirmation+ attribute)
  22. #
  23. # If confirmation validation is not needed, simply leave out the
  24. # value for +XXX_confirmation+ (i.e. don't provide a form field for
  25. # it). When this attribute has a +nil+ value, the validation will not be
  26. # triggered.
  27. #
  28. # For further customizability, it is possible to suppress the default
  29. # validations by passing <tt>validations: false</tt> as an argument.
  30. #
  31. # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
  32. #
  33. # gem 'bcrypt', '~> 3.1.7'
  34. #
  35. # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
  36. #
  37. # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
  38. # class User < ActiveRecord::Base
  39. # has_secure_password
  40. # has_secure_password :recovery_password, validations: false
  41. # end
  42. #
  43. # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
  44. # user.save # => false, password required
  45. # user.password = 'mUc3m00RsqyRe'
  46. # user.save # => false, confirmation doesn't match
  47. # user.password_confirmation = 'mUc3m00RsqyRe'
  48. # user.save # => true
  49. # user.recovery_password = "42password"
  50. # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
  51. # user.save # => true
  52. # user.authenticate('notright') # => false
  53. # user.authenticate('mUc3m00RsqyRe') # => user
  54. # user.authenticate_recovery_password('42password') # => user
  55. # User.find_by(name: 'david')&.authenticate('notright') # => false
  56. # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
  57. 1 def has_secure_password(attribute = :password, validations: true)
  58. # Load bcrypt gem only when has_secure_password is used.
  59. # This is to avoid ActiveModel (and by extension the entire framework)
  60. # being dependent on a binary library.
  61. 3 begin
  62. 3 require "bcrypt"
  63. rescue LoadError
  64. $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
  65. raise
  66. end
  67. 3 include InstanceMethodsOnActivation.new(attribute)
  68. 3 if validations
  69. 1 include ActiveModel::Validations
  70. # This ensures the model has a password by checking whether the password_digest
  71. # is present, so that this works with both new and existing records. However,
  72. # when there is an error, the message is added to the password attribute instead
  73. # so that the error message will make sense to the end-user.
  74. 1 validate do |record|
  75. 22 record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
  76. end
  77. 1 validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
  78. 1 validates_confirmation_of attribute, allow_blank: true
  79. end
  80. end
  81. end
  82. 1 class InstanceMethodsOnActivation < Module
  83. 1 def initialize(attribute)
  84. 3 attr_reader attribute
  85. 3 define_method("#{attribute}=") do |unencrypted_password|
  86. 28 if unencrypted_password.nil?
  87. 3 self.send("#{attribute}_digest=", nil)
  88. 25 elsif !unencrypted_password.empty?
  89. 21 instance_variable_set("@#{attribute}", unencrypted_password)
  90. 21 cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
  91. 21 self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
  92. end
  93. end
  94. 3 define_method("#{attribute}_confirmation=") do |unencrypted_password|
  95. 13 instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
  96. end
  97. # Returns +self+ if the password is correct, otherwise +false+.
  98. #
  99. # class User < ActiveRecord::Base
  100. # has_secure_password validations: false
  101. # end
  102. #
  103. # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
  104. # user.save
  105. # user.authenticate_password('notright') # => false
  106. # user.authenticate_password('mUc3m00RsqyRe') # => user
  107. 3 define_method("authenticate_#{attribute}") do |unencrypted_password|
  108. 6 attribute_digest = send("#{attribute}_digest")
  109. 6 BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
  110. end
  111. 3 alias_method :authenticate, :authenticate_password if attribute == :password
  112. end
  113. end
  114. end
  115. end

lib/active_model/serialization.rb

100.0% lines covered

27 relevant lines. 27 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. # == Active \Model \Serialization
  4. #
  5. # Provides a basic serialization to a serializable_hash for your objects.
  6. #
  7. # A minimal implementation could be:
  8. #
  9. # class Person
  10. # include ActiveModel::Serialization
  11. #
  12. # attr_accessor :name
  13. #
  14. # def attributes
  15. # {'name' => nil}
  16. # end
  17. # end
  18. #
  19. # Which would provide you with:
  20. #
  21. # person = Person.new
  22. # person.serializable_hash # => {"name"=>nil}
  23. # person.name = "Bob"
  24. # person.serializable_hash # => {"name"=>"Bob"}
  25. #
  26. # An +attributes+ hash must be defined and should contain any attributes you
  27. # need to be serialized. Attributes must be strings, not symbols.
  28. # When called, serializable hash will use instance methods that match the name
  29. # of the attributes hash's keys. In order to override this behavior, take a look
  30. # at the private method +read_attribute_for_serialization+.
  31. #
  32. # ActiveModel::Serializers::JSON module automatically includes
  33. # the <tt>ActiveModel::Serialization</tt> module, so there is no need to
  34. # explicitly include <tt>ActiveModel::Serialization</tt>.
  35. #
  36. # A minimal implementation including JSON would be:
  37. #
  38. # class Person
  39. # include ActiveModel::Serializers::JSON
  40. #
  41. # attr_accessor :name
  42. #
  43. # def attributes
  44. # {'name' => nil}
  45. # end
  46. # end
  47. #
  48. # Which would provide you with:
  49. #
  50. # person = Person.new
  51. # person.serializable_hash # => {"name"=>nil}
  52. # person.as_json # => {"name"=>nil}
  53. # person.to_json # => "{\"name\":null}"
  54. #
  55. # person.name = "Bob"
  56. # person.serializable_hash # => {"name"=>"Bob"}
  57. # person.as_json # => {"name"=>"Bob"}
  58. # person.to_json # => "{\"name\":\"Bob\"}"
  59. #
  60. # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
  61. # <tt>:include</tt>. The following are all valid examples:
  62. #
  63. # person.serializable_hash(only: 'name')
  64. # person.serializable_hash(include: :address)
  65. # person.serializable_hash(include: { address: { only: 'city' }})
  66. 1 module Serialization
  67. # Returns a serialized hash of your object.
  68. #
  69. # class Person
  70. # include ActiveModel::Serialization
  71. #
  72. # attr_accessor :name, :age
  73. #
  74. # def attributes
  75. # {'name' => nil, 'age' => nil}
  76. # end
  77. #
  78. # def capitalized_name
  79. # name.capitalize
  80. # end
  81. # end
  82. #
  83. # person = Person.new
  84. # person.name = 'bob'
  85. # person.age = 22
  86. # person.serializable_hash # => {"name"=>"bob", "age"=>22}
  87. # person.serializable_hash(only: :name) # => {"name"=>"bob"}
  88. # person.serializable_hash(except: :name) # => {"age"=>22}
  89. # person.serializable_hash(methods: :capitalized_name)
  90. # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
  91. #
  92. # Example with <tt>:include</tt> option
  93. #
  94. # class User
  95. # include ActiveModel::Serializers::JSON
  96. # attr_accessor :name, :notes # Emulate has_many :notes
  97. # def attributes
  98. # {'name' => nil}
  99. # end
  100. # end
  101. #
  102. # class Note
  103. # include ActiveModel::Serializers::JSON
  104. # attr_accessor :title, :text
  105. # def attributes
  106. # {'title' => nil, 'text' => nil}
  107. # end
  108. # end
  109. #
  110. # note = Note.new
  111. # note.title = 'Battle of Austerlitz'
  112. # note.text = 'Some text here'
  113. #
  114. # user = User.new
  115. # user.name = 'Napoleon'
  116. # user.notes = [note]
  117. #
  118. # user.serializable_hash
  119. # # => {"name" => "Napoleon"}
  120. # user.serializable_hash(include: { notes: { only: 'title' }})
  121. # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
  122. 1 def serializable_hash(options = nil)
  123. 58 attribute_names = attributes.keys
  124. 58 return serializable_attributes(attribute_names) if options.blank?
  125. 39 if only = options[:only]
  126. 15 attribute_names &= Array(only).map(&:to_s)
  127. 24 elsif except = options[:except]
  128. 7 attribute_names -= Array(except).map(&:to_s)
  129. end
  130. 39 hash = serializable_attributes(attribute_names)
  131. 49 Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
  132. 38 serializable_add_includes(options) do |association, records, opts|
  133. 16 hash[association.to_s] = if records.respond_to?(:to_ary)
  134. 28 records.to_ary.map { |a| a.serializable_hash(opts) }
  135. else
  136. 5 records.serializable_hash(opts)
  137. end
  138. end
  139. 38 hash
  140. end
  141. 1 private
  142. # Hook method defining how an attribute value should be retrieved for
  143. # serialization. By default this is assumed to be an instance named after
  144. # the attribute. Override this method in subclasses should you need to
  145. # retrieve the value for a given attribute differently:
  146. #
  147. # class MyClass
  148. # include ActiveModel::Serialization
  149. #
  150. # def initialize(data = {})
  151. # @data = data
  152. # end
  153. #
  154. # def read_attribute_for_serialization(key)
  155. # @data[key]
  156. # end
  157. # end
  158. 1 alias :read_attribute_for_serialization :send
  159. 1 def serializable_attributes(attribute_names)
  160. 221 attribute_names.index_with { |n| read_attribute_for_serialization(n) }
  161. end
  162. # Add associations specified via the <tt>:include</tt> option.
  163. #
  164. # Expects a block that takes as arguments:
  165. # +association+ - name of the association
  166. # +records+ - the association record(s) to be serialized
  167. # +opts+ - options for the association records
  168. 1 def serializable_add_includes(options = {}) #:nodoc:
  169. 38 return unless includes = options[:include]
  170. 13 unless includes.is_a?(Hash)
  171. 20 includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
  172. end
  173. 13 includes.each do |association, opts|
  174. 16 if records = send(association)
  175. 16 yield association, records, opts
  176. end
  177. end
  178. end
  179. end
  180. end

lib/active_model/serializers/json.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/json"
  3. 1 module ActiveModel
  4. 1 module Serializers
  5. # == Active \Model \JSON \Serializer
  6. 1 module JSON
  7. 1 extend ActiveSupport::Concern
  8. 1 include ActiveModel::Serialization
  9. 1 included do
  10. 1 extend ActiveModel::Naming
  11. 1 class_attribute :include_root_in_json, instance_writer: false, default: false
  12. end
  13. # Returns a hash representing the model. Some configuration can be
  14. # passed through +options+.
  15. #
  16. # The option <tt>include_root_in_json</tt> controls the top-level behavior
  17. # of +as_json+. If +true+, +as_json+ will emit a single root node named
  18. # after the object's type. The default value for <tt>include_root_in_json</tt>
  19. # option is +false+.
  20. #
  21. # user = User.find(1)
  22. # user.as_json
  23. # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  24. # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
  25. #
  26. # ActiveRecord::Base.include_root_in_json = true
  27. #
  28. # user.as_json
  29. # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  30. # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
  31. #
  32. # This behavior can also be achieved by setting the <tt>:root</tt> option
  33. # to +true+ as in:
  34. #
  35. # user = User.find(1)
  36. # user.as_json(root: true)
  37. # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  38. # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
  39. #
  40. # If you prefer, <tt>:root</tt> may also be set to a custom string key instead as in:
  41. #
  42. # user = User.find(1)
  43. # user.as_json(root: "author")
  44. # # => { "author" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  45. # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
  46. #
  47. # Without any +options+, the returned Hash will include all the model's
  48. # attributes.
  49. #
  50. # user = User.find(1)
  51. # user.as_json
  52. # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  53. # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
  54. #
  55. # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
  56. # the attributes included, and work similar to the +attributes+ method.
  57. #
  58. # user.as_json(only: [:id, :name])
  59. # # => { "id" => 1, "name" => "Konata Izumi" }
  60. #
  61. # user.as_json(except: [:id, :created_at, :age])
  62. # # => { "name" => "Konata Izumi", "awesome" => true }
  63. #
  64. # To include the result of some method calls on the model use <tt>:methods</tt>:
  65. #
  66. # user.as_json(methods: :permalink)
  67. # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  68. # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
  69. # # "permalink" => "1-konata-izumi" }
  70. #
  71. # To include associations use <tt>:include</tt>:
  72. #
  73. # user.as_json(include: :posts)
  74. # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  75. # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
  76. # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
  77. # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
  78. #
  79. # Second level and higher order associations work as well:
  80. #
  81. # user.as_json(include: { posts: {
  82. # include: { comments: {
  83. # only: :body } },
  84. # only: :title } })
  85. # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
  86. # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
  87. # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
  88. # # "title" => "Welcome to the weblog" },
  89. # # { "comments" => [ { "body" => "Don't think too hard" } ],
  90. # # "title" => "So I was thinking" } ] }
  91. 1 def as_json(options = nil)
  92. 16 root = if options && options.key?(:root)
  93. 4 options[:root]
  94. else
  95. 12 include_root_in_json
  96. end
  97. 16 hash = serializable_hash(options).as_json
  98. 16 if root
  99. 5 root = model_name.element if root == true
  100. 5 { root => hash }
  101. else
  102. 11 hash
  103. end
  104. end
  105. # Sets the model +attributes+ from a JSON string. Returns +self+.
  106. #
  107. # class Person
  108. # include ActiveModel::Serializers::JSON
  109. #
  110. # attr_accessor :name, :age, :awesome
  111. #
  112. # def attributes=(hash)
  113. # hash.each do |key, value|
  114. # send("#{key}=", value)
  115. # end
  116. # end
  117. #
  118. # def attributes
  119. # instance_values
  120. # end
  121. # end
  122. #
  123. # json = { name: 'bob', age: 22, awesome:true }.to_json
  124. # person = Person.new
  125. # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
  126. # person.name # => "bob"
  127. # person.age # => 22
  128. # person.awesome # => true
  129. #
  130. # The default value for +include_root+ is +false+. You can change it to
  131. # +true+ if the given JSON string includes a single root node.
  132. #
  133. # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json
  134. # person = Person.new
  135. # person.from_json(json, true) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
  136. # person.name # => "bob"
  137. # person.age # => 22
  138. # person.awesome # => true
  139. 1 def from_json(json, include_root = include_root_in_json)
  140. 3 hash = ActiveSupport::JSON.decode(json)
  141. 3 hash = hash.values.first if include_root
  142. 3 self.attributes = hash
  143. 3 self
  144. end
  145. end
  146. end
  147. end

lib/active_model/translation.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. # == Active \Model \Translation
  4. #
  5. # Provides integration between your object and the Rails internationalization
  6. # (i18n) framework.
  7. #
  8. # A minimal implementation could be:
  9. #
  10. # class TranslatedPerson
  11. # extend ActiveModel::Translation
  12. # end
  13. #
  14. # TranslatedPerson.human_attribute_name('my_attribute')
  15. # # => "My attribute"
  16. #
  17. # This also provides the required class methods for hooking into the
  18. # Rails internationalization API, including being able to define a
  19. # class based +i18n_scope+ and +lookup_ancestors+ to find translations in
  20. # parent classes.
  21. 1 module Translation
  22. 1 include ActiveModel::Naming
  23. # Returns the +i18n_scope+ for the class. Overwrite if you want custom lookup.
  24. 1 def i18n_scope
  25. 1636 :activemodel
  26. end
  27. # When localizing a string, it goes through the lookup returned by this
  28. # method, which is used in ActiveModel::Name#human,
  29. # ActiveModel::Errors#full_messages and
  30. # ActiveModel::Translation#human_attribute_name.
  31. 1 def lookup_ancestors
  32. 23163 ancestors.select { |x| x.respond_to?(:model_name) }
  33. end
  34. # Transforms attribute names into a more human format, such as "First name"
  35. # instead of "first_name".
  36. #
  37. # Person.human_attribute_name("first_name") # => "First name"
  38. #
  39. # Specify +options+ with additional translating options.
  40. 1 def human_attribute_name(attribute, options = {})
  41. 586 options = { count: 1 }.merge!(options)
  42. 586 parts = attribute.to_s.split(".")
  43. 586 attribute = parts.pop
  44. 586 namespace = parts.join("/") unless parts.empty?
  45. 586 attributes_scope = "#{i18n_scope}.attributes"
  46. 586 if namespace
  47. 19 defaults = lookup_ancestors.map do |klass|
  48. 34 :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
  49. end
  50. 19 defaults << :"#{attributes_scope}.#{namespace}.#{attribute}"
  51. else
  52. 567 defaults = lookup_ancestors.map do |klass|
  53. 641 :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}"
  54. end
  55. end
  56. 586 defaults << :"attributes.#{attribute}"
  57. 586 defaults << options.delete(:default) if options[:default]
  58. 586 defaults << attribute.humanize
  59. 586 options[:default] = defaults
  60. 586 I18n.translate(defaults.shift, **options)
  61. end
  62. end
  63. end

lib/active_model/type.rb

100.0% lines covered

36 relevant lines. 36 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/type/helpers"
  3. 1 require "active_model/type/value"
  4. 1 require "active_model/type/big_integer"
  5. 1 require "active_model/type/binary"
  6. 1 require "active_model/type/boolean"
  7. 1 require "active_model/type/date"
  8. 1 require "active_model/type/date_time"
  9. 1 require "active_model/type/decimal"
  10. 1 require "active_model/type/float"
  11. 1 require "active_model/type/immutable_string"
  12. 1 require "active_model/type/integer"
  13. 1 require "active_model/type/string"
  14. 1 require "active_model/type/time"
  15. 1 require "active_model/type/registry"
  16. 1 module ActiveModel
  17. 1 module Type
  18. 1 @registry = Registry.new
  19. 1 class << self
  20. 1 attr_accessor :registry # :nodoc:
  21. # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
  22. 1 def register(type_name, klass = nil, **options, &block)
  23. 11 registry.register(type_name, klass, **options, &block)
  24. end
  25. 1 def lookup(*args, **kwargs) # :nodoc:
  26. 10 registry.lookup(*args, **kwargs)
  27. end
  28. 1 def default_value # :nodoc:
  29. 13 @default_value ||= Value.new
  30. end
  31. end
  32. 1 register(:big_integer, Type::BigInteger)
  33. 1 register(:binary, Type::Binary)
  34. 1 register(:boolean, Type::Boolean)
  35. 1 register(:date, Type::Date)
  36. 1 register(:datetime, Type::DateTime)
  37. 1 register(:decimal, Type::Decimal)
  38. 1 register(:float, Type::Float)
  39. 1 register(:immutable_string, Type::ImmutableString)
  40. 1 register(:integer, Type::Integer)
  41. 1 register(:string, Type::String)
  42. 1 register(:time, Type::Time)
  43. end
  44. end

lib/active_model/type/big_integer.rb

100.0% lines covered

7 relevant lines. 7 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/type/integer"
  3. 1 module ActiveModel
  4. 1 module Type
  5. 1 class BigInteger < Integer # :nodoc:
  6. 1 private
  7. 1 def max_value
  8. 6 ::Float::INFINITY
  9. end
  10. end
  11. end
  12. end

lib/active_model/type/binary.rb

59.26% lines covered

27 relevant lines. 16 lines covered and 11 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 class Binary < Value # :nodoc:
  5. 1 def type
  6. :binary
  7. end
  8. 1 def binary?
  9. true
  10. end
  11. 1 def cast(value)
  12. 3 if value.is_a?(Data)
  13. value.to_s
  14. else
  15. 3 super
  16. end
  17. end
  18. 1 def serialize(value)
  19. return if value.nil?
  20. Data.new(super)
  21. end
  22. 1 def changed_in_place?(raw_old_value, value)
  23. old_value = deserialize(raw_old_value)
  24. old_value != value
  25. end
  26. 1 class Data # :nodoc:
  27. 1 def initialize(value)
  28. @value = value.to_s
  29. end
  30. 1 def to_s
  31. @value
  32. end
  33. 1 alias_method :to_str, :to_s
  34. 1 def hex
  35. @value.unpack1("H*")
  36. end
  37. 1 def ==(other)
  38. other == to_s || super
  39. end
  40. end
  41. end
  42. end
  43. end

lib/active_model/type/boolean.rb

83.33% lines covered

12 relevant lines. 10 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. # == Active \Model \Type \Boolean
  5. #
  6. # A class that behaves like a boolean type, including rules for coercion of user input.
  7. #
  8. # === Coercion
  9. # Values set from user input will first be coerced into the appropriate ruby type.
  10. # Coercion behavior is roughly mapped to Ruby's boolean semantics.
  11. #
  12. # - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+
  13. # - Empty strings are coerced to +nil+
  14. # - All other values will be coerced to +true+
  15. 1 class Boolean < Value
  16. 1 FALSE_VALUES = [
  17. false, 0,
  18. "0", :"0",
  19. "f", :f,
  20. "F", :F,
  21. "false", :false,
  22. "FALSE", :FALSE,
  23. "off", :off,
  24. "OFF", :OFF,
  25. ].to_set.freeze
  26. 1 def type # :nodoc:
  27. :boolean
  28. end
  29. 1 def serialize(value) # :nodoc:
  30. cast(value)
  31. end
  32. 1 private
  33. 1 def cast_value(value)
  34. 40 if value == ""
  35. nil
  36. else
  37. 39 !FALSE_VALUES.include?(value)
  38. end
  39. end
  40. end
  41. end
  42. end

lib/active_model/type/date.rb

89.66% lines covered

29 relevant lines. 26 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 class Date < Value # :nodoc:
  5. 1 include Helpers::Timezone
  6. 1 include Helpers::AcceptsMultiparameterTime.new
  7. 1 def type
  8. :date
  9. end
  10. 1 def type_cast_for_schema(value)
  11. value.to_s(:db).inspect
  12. end
  13. 1 private
  14. 1 def cast_value(value)
  15. 8 if value.is_a?(::String)
  16. 4 return if value.empty?
  17. 3 fast_string_to_date(value) || fallback_string_to_date(value)
  18. 4 elsif value.respond_to?(:to_date)
  19. 4 value.to_date
  20. else
  21. value
  22. end
  23. end
  24. 1 ISO_DATE = /\A(\d{4})-(\d\d)-(\d\d)\z/
  25. 1 def fast_string_to_date(string)
  26. 3 if string =~ ISO_DATE
  27. 1 new_date $1.to_i, $2.to_i, $3.to_i
  28. end
  29. end
  30. 1 def fallback_string_to_date(string)
  31. 2 new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
  32. end
  33. 1 def new_date(year, mon, mday)
  34. 5 unless year.nil? || (year == 0 && mon == 0 && mday == 0)
  35. 3 ::Date.new(year, mon, mday) rescue nil
  36. end
  37. end
  38. 1 def value_from_multiparameter_assignment(*)
  39. 2 time = super
  40. 2 time && new_date(time.year, time.mon, time.mday)
  41. end
  42. end
  43. end
  44. end

lib/active_model/type/date_time.rb

95.83% lines covered

24 relevant lines. 23 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 class DateTime < Value # :nodoc:
  5. 1 include Helpers::Timezone
  6. 1 include Helpers::TimeValue
  7. 1 include Helpers::AcceptsMultiparameterTime.new(
  8. defaults: { 4 => 0, 5 => 0 }
  9. )
  10. 1 def type
  11. :datetime
  12. end
  13. 1 private
  14. 1 def cast_value(value)
  15. 6 return apply_seconds_precision(value) unless value.is_a?(::String)
  16. 6 return if value.empty?
  17. 5 fast_string_to_time(value) || fallback_string_to_time(value)
  18. end
  19. # '0.123456' -> 123456
  20. # '1.123456' -> 123456
  21. 1 def microseconds(time)
  22. 4 time[:sec_fraction] ? (time[:sec_fraction] * 1_000_000).to_i : 0
  23. end
  24. 1 def fallback_string_to_time(string)
  25. 4 time_hash = ::Date._parse(string)
  26. 4 time_hash[:sec_fraction] = microseconds(time_hash)
  27. 4 new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
  28. end
  29. 1 def value_from_multiparameter_assignment(values_hash)
  30. 8 missing_parameters = [1, 2, 3].delete_if { |key| values_hash.key?(key) }
  31. 2 unless missing_parameters.empty?
  32. 1 raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
  33. end
  34. 1 super
  35. end
  36. end
  37. end
  38. end

lib/active_model/type/decimal.rb

91.18% lines covered

34 relevant lines. 31 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "bigdecimal/util"
  3. 1 module ActiveModel
  4. 1 module Type
  5. 1 class Decimal < Value # :nodoc:
  6. 1 include Helpers::Numeric
  7. 1 BIGDECIMAL_PRECISION = 18
  8. 1 def type
  9. :decimal
  10. end
  11. 1 def type_cast_for_schema(value)
  12. value.to_s.inspect
  13. end
  14. 1 private
  15. 1 def cast_value(value)
  16. 17 casted_value = \
  17. case value
  18. when ::Float
  19. 5 convert_float_to_big_decimal(value)
  20. when ::Numeric
  21. 4 BigDecimal(value, precision || BIGDECIMAL_PRECISION)
  22. when ::String
  23. 6 begin
  24. 6 value.to_d
  25. rescue ArgumentError
  26. BigDecimal(0)
  27. end
  28. else
  29. 2 if value.respond_to?(:to_d)
  30. 1 value.to_d
  31. else
  32. 1 cast_value(value.to_s)
  33. end
  34. end
  35. 17 apply_scale(casted_value)
  36. end
  37. 1 def convert_float_to_big_decimal(value)
  38. 5 if precision
  39. 2 BigDecimal(apply_scale(value), float_precision)
  40. else
  41. 3 value.to_d
  42. end
  43. end
  44. 1 def float_precision
  45. 2 if precision.to_i > ::Float::DIG + 1
  46. 1 ::Float::DIG + 1
  47. else
  48. 1 precision.to_i
  49. end
  50. end
  51. 1 def apply_scale(value)
  52. 19 if scale
  53. 4 value.round(scale)
  54. else
  55. 15 value
  56. end
  57. end
  58. end
  59. end
  60. end

lib/active_model/type/float.rb

52.38% lines covered

21 relevant lines. 11 lines covered and 10 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/object/try"
  3. 1 module ActiveModel
  4. 1 module Type
  5. 1 class Float < Value # :nodoc:
  6. 1 include Helpers::Numeric
  7. 1 def type
  8. :float
  9. end
  10. 1 def type_cast_for_schema(value)
  11. return "::Float::NAN" if value.try(:nan?)
  12. case value
  13. when ::Float::INFINITY then "::Float::INFINITY"
  14. when -::Float::INFINITY then "-::Float::INFINITY"
  15. else super
  16. end
  17. end
  18. 1 private
  19. 1 def cast_value(value)
  20. 9 case value
  21. when ::Float then value
  22. when "Infinity" then ::Float::INFINITY
  23. when "-Infinity" then -::Float::INFINITY
  24. when "NaN" then ::Float::NAN
  25. 9 else value.to_f
  26. end
  27. end
  28. end
  29. end
  30. end

lib/active_model/type/helpers.rb

100.0% lines covered

5 relevant lines. 5 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/type/helpers/accepts_multiparameter_time"
  3. 1 require "active_model/type/helpers/numeric"
  4. 1 require "active_model/type/helpers/mutable"
  5. 1 require "active_model/type/helpers/time_value"
  6. 1 require "active_model/type/helpers/timezone"

lib/active_model/type/helpers/accepts_multiparameter_time.rb

80.77% lines covered

26 relevant lines. 21 lines covered and 5 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 module Helpers # :nodoc: all
  5. 1 class AcceptsMultiparameterTime < Module
  6. 1 module InstanceMethods
  7. 1 def serialize(value)
  8. super(cast(value))
  9. end
  10. 1 def cast(value)
  11. 27 if value.is_a?(Hash)
  12. 5 value_from_multiparameter_assignment(value)
  13. else
  14. 22 super(value)
  15. end
  16. end
  17. 1 def assert_valid_value(value)
  18. if value.is_a?(Hash)
  19. value_from_multiparameter_assignment(value)
  20. else
  21. super(value)
  22. end
  23. end
  24. 1 def value_constructed_by_mass_assignment?(value)
  25. value.is_a?(Hash)
  26. end
  27. end
  28. 1 def initialize(defaults: {})
  29. 3 include InstanceMethods
  30. 3 define_method(:value_from_multiparameter_assignment) do |values_hash|
  31. 4 defaults.each do |k, v|
  32. 7 values_hash[k] ||= v
  33. end
  34. 4 return unless values_hash[1] && values_hash[2] && values_hash[3]
  35. 4 values = values_hash.sort.map!(&:last)
  36. 4 ::Time.send(default_timezone, *values)
  37. end
  38. 3 private :value_from_multiparameter_assignment
  39. end
  40. end
  41. end
  42. end
  43. end

lib/active_model/type/helpers/mutable.rb

75.0% lines covered

8 relevant lines. 6 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 module Helpers # :nodoc: all
  5. 1 module Mutable
  6. 1 def cast(value)
  7. deserialize(serialize(value))
  8. end
  9. # +raw_old_value+ will be the `_before_type_cast` version of the
  10. # value (likely a string). +new_value+ will be the current, type
  11. # cast value.
  12. 1 def changed_in_place?(raw_old_value, new_value)
  13. raw_old_value != serialize(new_value)
  14. end
  15. end
  16. end
  17. end
  18. end

lib/active_model/type/helpers/numeric.rb

100.0% lines covered

23 relevant lines. 23 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 module Helpers # :nodoc: all
  5. 1 module Numeric
  6. 1 def serialize(value)
  7. 34 cast(value)
  8. end
  9. 1 def cast(value)
  10. # Checks whether the value is numeric. Spaceship operator
  11. # will return nil if value is not numeric.
  12. 93 value = if value <=> 0
  13. 37 value
  14. else
  15. 56 case value
  16. 2 when true then 1
  17. 2 when false then 0
  18. 52 else value.presence
  19. end
  20. end
  21. 93 super(value)
  22. end
  23. 1 def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
  24. 28 super || number_to_non_number?(old_value, new_value_before_type_cast)
  25. end
  26. 1 private
  27. 1 def number_to_non_number?(old_value, new_value_before_type_cast)
  28. 21 old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
  29. end
  30. 1 def non_numeric_string?(value)
  31. # 'wibble'.to_i will give zero, we want to make sure
  32. # that we aren't marking int zero to string zero as
  33. # changed.
  34. 24 !NUMERIC_REGEX.match?(value)
  35. end
  36. 1 NUMERIC_REGEX = /\A\s*[+-]?\d/
  37. 1 private_constant :NUMERIC_REGEX
  38. end
  39. end
  40. end
  41. end

lib/active_model/type/helpers/time_value.rb

65.22% lines covered

46 relevant lines. 30 lines covered and 16 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/string/zones"
  3. 1 require "active_support/core_ext/time/zones"
  4. 1 module ActiveModel
  5. 1 module Type
  6. 1 module Helpers # :nodoc: all
  7. 1 module TimeValue
  8. 1 def serialize(value)
  9. value = apply_seconds_precision(value)
  10. if value.acts_like?(:time)
  11. if is_utc?
  12. value = value.getutc if value.respond_to?(:getutc) && !value.utc?
  13. else
  14. value = value.getlocal if value.respond_to?(:getlocal)
  15. end
  16. end
  17. value
  18. end
  19. 1 def apply_seconds_precision(value)
  20. return value unless precision && value.respond_to?(:nsec)
  21. number_of_insignificant_digits = 9 - precision
  22. round_power = 10**number_of_insignificant_digits
  23. rounded_off_nsec = value.nsec % round_power
  24. if rounded_off_nsec > 0
  25. value.change(nsec: value.nsec - rounded_off_nsec)
  26. else
  27. value
  28. end
  29. end
  30. 1 def type_cast_for_schema(value)
  31. value.to_s(:db).inspect
  32. end
  33. 1 def user_input_in_time_zone(value)
  34. 2 value.in_time_zone
  35. end
  36. 1 private
  37. 1 def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
  38. # Treat 0000-00-00 00:00:00 as nil.
  39. 8 return if year.nil? || (year == 0 && mon == 0 && mday == 0)
  40. 6 if offset
  41. 4 time = ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
  42. 4 return unless time
  43. 4 time -= offset unless offset == 0
  44. 4 is_utc? ? time : time.getlocal
  45. 2 elsif is_utc?
  46. 2 ::Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
  47. else
  48. ::Time.local(year, mon, mday, hour, min, sec, microsec) rescue nil
  49. end
  50. end
  51. 1 ISO_DATETIME = /
  52. \A
  53. (\d{4})-(\d\d)-(\d\d)(?:T|\s) # 2020-06-20T
  54. (\d\d):(\d\d):(\d\d)(?:\.(\d{1,6})\d*)? # 10:20:30.123456
  55. (?:(Z(?=\z)|[+-]\d\d)(?::?(\d\d))?)? # +09:00
  56. \z
  57. /x
  58. 1 def fast_string_to_time(string)
  59. 9 return unless ISO_DATETIME =~ string
  60. 3 usec = $7.to_i
  61. 3 usec_len = $7&.length
  62. 3 if usec_len&.< 6
  63. usec *= 10 ** (6 - usec_len)
  64. end
  65. 3 if $8
  66. 1 offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
  67. end
  68. 3 new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
  69. end
  70. end
  71. end
  72. end
  73. end

lib/active_model/type/helpers/timezone.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/time/zones"
  3. 1 module ActiveModel
  4. 1 module Type
  5. 1 module Helpers # :nodoc: all
  6. 1 module Timezone
  7. 1 def is_utc?
  8. 10 ::Time.zone_default.nil? || ::Time.zone_default.match?("UTC")
  9. end
  10. 1 def default_timezone
  11. 4 is_utc? ? :utc : :local
  12. end
  13. end
  14. end
  15. end
  16. end

lib/active_model/type/immutable_string.rb

71.43% lines covered

21 relevant lines. 15 lines covered and 6 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 class ImmutableString < Value # :nodoc:
  5. 1 def initialize(**args)
  6. 17 @true = -(args.delete(:true)&.to_s || "t")
  7. 17 @false = -(args.delete(:false)&.to_s || "f")
  8. 17 super
  9. end
  10. 1 def type
  11. :string
  12. end
  13. 1 def serialize(value)
  14. 36 case value
  15. when ::Numeric, ::Symbol, ActiveSupport::Duration then value.to_s
  16. when true then @true
  17. when false then @false
  18. 36 else super
  19. end
  20. end
  21. 1 private
  22. 1 def cast_value(value)
  23. 3 case value
  24. when true then @true
  25. when false then @false
  26. 3 else value.to_s.freeze
  27. end
  28. end
  29. end
  30. end
  31. end

lib/active_model/type/integer.rb

91.43% lines covered

35 relevant lines. 32 lines covered and 3 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 class Integer < Value # :nodoc:
  5. 1 include Helpers::Numeric
  6. # Column storage size in bytes.
  7. # 4 bytes means an integer as opposed to smallint etc.
  8. 1 DEFAULT_LIMIT = 4
  9. 1 def initialize(**)
  10. 55 super
  11. 55 @range = min_value...max_value
  12. end
  13. 1 def type
  14. :integer
  15. end
  16. 1 def deserialize(value)
  17. 20 return if value.blank?
  18. 18 value.to_i
  19. end
  20. 1 def serialize(value)
  21. 36 return if value.is_a?(::String) && non_numeric_string?(value)
  22. 34 ensure_in_range(super)
  23. end
  24. 1 def serializable?(value)
  25. cast_value = cast(value)
  26. in_range?(cast_value) && super
  27. end
  28. 1 private
  29. 1 attr_reader :range
  30. 1 def in_range?(value)
  31. 34 !value || range.member?(value)
  32. end
  33. 51 def cast_value(value)
  34. 6 value.to_i rescue nil
  35. end
  36. 1 def ensure_in_range(value)
  37. 34 unless in_range?(value)
  38. 6 raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
  39. end
  40. 28 value
  41. end
  42. 1 def max_value
  43. 104 1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
  44. end
  45. 1 def min_value
  46. 55 -max_value
  47. end
  48. 1 def _limit
  49. 110 limit || DEFAULT_LIMIT
  50. end
  51. end
  52. end
  53. end

lib/active_model/type/registry.rb

94.44% lines covered

36 relevant lines. 34 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. # :stopdoc:
  4. 1 module Type
  5. 1 class Registry
  6. 1 def initialize
  7. 4 @registrations = []
  8. end
  9. 1 def initialize_dup(other)
  10. @registrations = @registrations.dup
  11. super
  12. end
  13. 1 def register(type_name, klass = nil, **options, &block)
  14. 16 unless block_given?
  15. 25 block = proc { |_, *args| klass.new(*args) }
  16. 13 block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
  17. end
  18. 16 registrations << registration_klass.new(type_name, block, **options)
  19. end
  20. 1 def lookup(symbol, *args, **kwargs)
  21. 17 registration = find_registration(symbol, *args, **kwargs)
  22. 17 if registration
  23. 16 registration.call(self, symbol, *args, **kwargs)
  24. else
  25. 1 raise ArgumentError, "Unknown type #{symbol.inspect}"
  26. end
  27. end
  28. 1 private
  29. 1 attr_reader :registrations
  30. 1 def registration_klass
  31. 16 Registration
  32. end
  33. 1 def find_registration(symbol, *args, **kwargs)
  34. 108 registrations.find { |r| r.matches?(symbol, *args, **kwargs) }
  35. end
  36. end
  37. 1 class Registration
  38. # Options must be taken because of https://bugs.ruby-lang.org/issues/10856
  39. 1 def initialize(name, block, **)
  40. 16 @name = name
  41. 16 @block = block
  42. end
  43. 1 def call(_registry, *args, **kwargs)
  44. 16 if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
  45. 1 block.call(*args, **kwargs)
  46. else
  47. 15 block.call(*args)
  48. end
  49. end
  50. 1 def matches?(type_name, *args, **kwargs)
  51. 91 type_name == name
  52. end
  53. 1 private
  54. 1 attr_reader :name, :block
  55. end
  56. end
  57. # :startdoc:
  58. end

lib/active_model/type/string.rb

93.75% lines covered

16 relevant lines. 15 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/type/immutable_string"
  3. 1 module ActiveModel
  4. 1 module Type
  5. 1 class String < ImmutableString # :nodoc:
  6. 1 def changed_in_place?(raw_old_value, new_value)
  7. 6 if new_value.is_a?(::String)
  8. 5 raw_old_value != new_value
  9. end
  10. end
  11. 1 def to_immutable_string
  12. ImmutableString.new(
  13. true: @true,
  14. false: @false,
  15. limit: limit,
  16. precision: precision,
  17. scale: scale,
  18. )
  19. end
  20. 1 private
  21. 1 def cast_value(value)
  22. 78 case value
  23. 74 when ::String then ::String.new(value)
  24. 1 when true then @true
  25. 1 when false then @false
  26. 2 else value.to_s
  27. end
  28. end
  29. end
  30. end
  31. end

lib/active_model/type/time.rb

92.0% lines covered

25 relevant lines. 23 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 class Time < Value # :nodoc:
  5. 1 include Helpers::Timezone
  6. 1 include Helpers::TimeValue
  7. 1 include Helpers::AcceptsMultiparameterTime.new(
  8. defaults: { 1 => 2000, 2 => 1, 3 => 1, 4 => 0, 5 => 0 }
  9. )
  10. 1 def type
  11. :time
  12. end
  13. 1 def user_input_in_time_zone(value)
  14. 5 return unless value.present?
  15. 3 case value
  16. when ::String
  17. 3 value = "2000-01-01 #{value}"
  18. 3 time_hash = ::Date._parse(value)
  19. 3 return if time_hash[:hour].nil?
  20. when ::Time
  21. value = value.change(year: 2000, day: 1, month: 1)
  22. end
  23. 2 super(value)
  24. end
  25. 1 private
  26. 1 def cast_value(value)
  27. 5 return apply_seconds_precision(value) unless value.is_a?(::String)
  28. 5 return if value.empty?
  29. 4 dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ")
  30. 4 fast_string_to_time(dummy_time_value) || begin
  31. 2 time_hash = ::Date._parse(dummy_time_value)
  32. 2 return if time_hash[:hour].nil?
  33. 1 new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
  34. end
  35. end
  36. end
  37. end
  38. end

lib/active_model/type/value.rb

82.5% lines covered

40 relevant lines. 33 lines covered and 7 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Type
  4. 1 class Value
  5. 1 attr_reader :precision, :scale, :limit
  6. 1 def initialize(precision: nil, limit: nil, scale: nil)
  7. 130 @precision = precision
  8. 130 @scale = scale
  9. 130 @limit = limit
  10. end
  11. # Returns true if this type can convert +value+ to a type that is usable
  12. # by the database. For example a boolean type can return +true+ if the
  13. # value parameter is a Ruby boolean, but may return +false+ if the value
  14. # parameter is some other object.
  15. 1 def serializable?(value)
  16. true
  17. end
  18. 1 def type # :nodoc:
  19. end
  20. # Converts a value from database input to the appropriate ruby type. The
  21. # return value of this method will be returned from
  22. # ActiveRecord::AttributeMethods::Read#read_attribute. The default
  23. # implementation just calls Value#cast.
  24. #
  25. # +value+ The raw input, as provided from the database.
  26. 1 def deserialize(value)
  27. 45 cast(value)
  28. end
  29. # Type casts a value from user input (e.g. from a setter). This value may
  30. # be a string from the form builder, or a ruby object passed to a setter.
  31. # There is currently no way to differentiate between which source it came
  32. # from.
  33. #
  34. # The return value of this method will be returned from
  35. # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
  36. # Value#cast_value.
  37. #
  38. # +value+ The raw input, as provided to the attribute setter.
  39. 1 def cast(value)
  40. 253 cast_value(value) unless value.nil?
  41. end
  42. # Casts a value from the ruby type to a type that the database knows how
  43. # to understand. The returned value from this method should be a
  44. # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
  45. # +nil+.
  46. 1 def serialize(value)
  47. 36 value
  48. end
  49. # Type casts a value for schema dumping. This method is private, as we are
  50. # hoping to remove it entirely.
  51. 1 def type_cast_for_schema(value) # :nodoc:
  52. value.inspect
  53. end
  54. # These predicates are not documented, as I need to look further into
  55. # their use, and see if they can be removed entirely.
  56. 1 def binary? # :nodoc:
  57. false
  58. end
  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. 1 def changed?(old_value, new_value, _new_value_before_type_cast)
  63. 65 old_value != new_value
  64. end
  65. # Determines whether the mutable value has been modified since it was
  66. # read. Returns +false+ by default. If your type returns an object
  67. # which could be mutated, you should override this method. You will need
  68. # to either:
  69. #
  70. # - pass +new_value+ to Value#serialize and compare it to
  71. # +raw_old_value+
  72. #
  73. # or
  74. #
  75. # - pass +raw_old_value+ to Value#deserialize and compare it to
  76. # +new_value+
  77. #
  78. # +raw_old_value+ The original value, before being passed to
  79. # +deserialize+.
  80. #
  81. # +new_value+ The current value, after type casting.
  82. 1 def changed_in_place?(raw_old_value, new_value)
  83. 3 false
  84. end
  85. 1 def value_constructed_by_mass_assignment?(_value) # :nodoc:
  86. false
  87. end
  88. 1 def force_equality?(_value) # :nodoc:
  89. false
  90. end
  91. 1 def map(value) # :nodoc:
  92. yield value
  93. end
  94. 1 def ==(other)
  95. 13 self.class == other.class &&
  96. precision == other.precision &&
  97. scale == other.scale &&
  98. limit == other.limit
  99. end
  100. 1 alias eql? ==
  101. 1 def hash
  102. [self.class, precision, scale, limit].hash
  103. end
  104. 1 def assert_valid_value(_)
  105. end
  106. 1 private
  107. # Convenience method for types which do not need separate type casting
  108. # behavior for user and database inputs. Called by Value#cast for
  109. # values except +nil+.
  110. 1 def cast_value(value) # :doc:
  111. 11 value
  112. end
  113. end
  114. end
  115. end

lib/active_model/validations.rb

100.0% lines covered

74 relevant lines. 74 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/array/extract_options"
  3. 1 module ActiveModel
  4. # == Active \Model \Validations
  5. #
  6. # Provides a full validation framework to your objects.
  7. #
  8. # A minimal implementation could be:
  9. #
  10. # class Person
  11. # include ActiveModel::Validations
  12. #
  13. # attr_accessor :first_name, :last_name
  14. #
  15. # validates_each :first_name, :last_name do |record, attr, value|
  16. # record.errors.add attr, "starts with z." if value.start_with?("z")
  17. # end
  18. # end
  19. #
  20. # Which provides you with the full standard validation stack that you
  21. # know from Active Record:
  22. #
  23. # person = Person.new
  24. # person.valid? # => true
  25. # person.invalid? # => false
  26. #
  27. # person.first_name = 'zoolander'
  28. # person.valid? # => false
  29. # person.invalid? # => true
  30. # person.errors.messages # => {first_name:["starts with z."]}
  31. #
  32. # Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+
  33. # method to your instances initialized with a new <tt>ActiveModel::Errors</tt>
  34. # object, so there is no need for you to do this manually.
  35. 1 module Validations
  36. 1 extend ActiveSupport::Concern
  37. 1 included do
  38. 14 extend ActiveModel::Naming
  39. 14 extend ActiveModel::Callbacks
  40. 14 extend ActiveModel::Translation
  41. 14 extend HelperMethods
  42. 14 include HelperMethods
  43. 14 attr_accessor :validation_context
  44. 14 private :validation_context=
  45. 14 define_callbacks :validate, scope: :name
  46. 367 class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
  47. end
  48. 1 module ClassMethods
  49. # Validates each attribute against a block.
  50. #
  51. # class Person
  52. # include ActiveModel::Validations
  53. #
  54. # attr_accessor :first_name, :last_name
  55. #
  56. # validates_each :first_name, :last_name, allow_blank: true do |record, attr, value|
  57. # record.errors.add attr, "starts with z." if value.start_with?("z")
  58. # end
  59. # end
  60. #
  61. # Options:
  62. # * <tt>:on</tt> - Specifies the contexts where this validation is active.
  63. # Runs in all validation contexts by default +nil+. You can pass a symbol
  64. # or an array of symbols. (e.g. <tt>on: :create</tt> or
  65. # <tt>on: :custom_validation_context</tt> or
  66. # <tt>on: [:create, :custom_validation_context]</tt>)
  67. # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
  68. # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
  69. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
  70. # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
  71. # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
  72. # proc or string should return or evaluate to a +true+ or +false+ value.
  73. # * <tt>:unless</tt> - Specifies a method, proc or string to call to
  74. # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
  75. # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
  76. # method, proc or string should return or evaluate to a +true+ or +false+
  77. # value.
  78. 1 def validates_each(*attr_names, &block)
  79. 2 validates_with BlockValidator, _merge_attributes(attr_names), &block
  80. end
  81. 1 VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
  82. # Adds a validation method or block to the class. This is useful when
  83. # overriding the +validate+ instance method becomes too unwieldy and
  84. # you're looking for more descriptive declaration of your validations.
  85. #
  86. # This can be done with a symbol pointing to a method:
  87. #
  88. # class Comment
  89. # include ActiveModel::Validations
  90. #
  91. # validate :must_be_friends
  92. #
  93. # def must_be_friends
  94. # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
  95. # end
  96. # end
  97. #
  98. # With a block which is passed with the current record to be validated:
  99. #
  100. # class Comment
  101. # include ActiveModel::Validations
  102. #
  103. # validate do |comment|
  104. # comment.must_be_friends
  105. # end
  106. #
  107. # def must_be_friends
  108. # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
  109. # end
  110. # end
  111. #
  112. # Or with a block where self points to the current record to be validated:
  113. #
  114. # class Comment
  115. # include ActiveModel::Validations
  116. #
  117. # validate do
  118. # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
  119. # end
  120. # end
  121. #
  122. # Note that the return value of validation methods is not relevant.
  123. # It's not possible to halt the validate callback chain.
  124. #
  125. # Options:
  126. # * <tt>:on</tt> - Specifies the contexts where this validation is active.
  127. # Runs in all validation contexts by default +nil+. You can pass a symbol
  128. # or an array of symbols. (e.g. <tt>on: :create</tt> or
  129. # <tt>on: :custom_validation_context</tt> or
  130. # <tt>on: [:create, :custom_validation_context]</tt>)
  131. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
  132. # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
  133. # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
  134. # proc or string should return or evaluate to a +true+ or +false+ value.
  135. # * <tt>:unless</tt> - Specifies a method, proc or string to call to
  136. # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
  137. # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
  138. # method, proc or string should return or evaluate to a +true+ or +false+
  139. # value.
  140. #
  141. # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
  142. #
  143. 1 def validate(*args, &block)
  144. 372 options = args.extract_options!
  145. 739 if args.all? { |arg| arg.is_a?(Symbol) }
  146. 15 options.each_key do |k|
  147. 7 unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
  148. 1 raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?")
  149. end
  150. end
  151. end
  152. 371 if options.key?(:on)
  153. 11 options = options.dup
  154. 11 options[:on] = Array(options[:on])
  155. 11 options[:if] = Array(options[:if])
  156. 11 options[:if].unshift ->(o) {
  157. 38 !(options[:on] & Array(o.validation_context)).empty?
  158. }
  159. end
  160. 371 set_callback(:validate, *args, options, &block)
  161. end
  162. # List all validators that are being used to validate the model using
  163. # +validates_with+ method.
  164. #
  165. # class Person
  166. # include ActiveModel::Validations
  167. #
  168. # validates_with MyValidator
  169. # validates_with OtherValidator, on: :create
  170. # validates_with StrictValidator, strict: true
  171. # end
  172. #
  173. # Person.validators
  174. # # => [
  175. # # #<MyValidator:0x007fbff403e808 @options={}>,
  176. # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
  177. # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
  178. # # ]
  179. 1 def validators
  180. 2 _validators.values.flatten.uniq
  181. end
  182. # Clears all of the validators and validations.
  183. #
  184. # Note that this will clear anything that is being used to validate
  185. # the model for both the +validates_with+ and +validate+ methods.
  186. # It clears the validators that are created with an invocation of
  187. # +validates_with+ and the callbacks that are set by an invocation
  188. # of +validate+.
  189. #
  190. # class Person
  191. # include ActiveModel::Validations
  192. #
  193. # validates_with MyValidator
  194. # validates_with OtherValidator, on: :create
  195. # validates_with StrictValidator, strict: true
  196. # validate :cannot_be_robot
  197. #
  198. # def cannot_be_robot
  199. # errors.add(:base, 'A person cannot be a robot') if person_is_robot
  200. # end
  201. # end
  202. #
  203. # Person.validators
  204. # # => [
  205. # # #<MyValidator:0x007fbff403e808 @options={}>,
  206. # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
  207. # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
  208. # # ]
  209. #
  210. # If one runs <tt>Person.clear_validators!</tt> and then checks to see what
  211. # validators this class has, you would obtain:
  212. #
  213. # Person.validators # => []
  214. #
  215. # Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased
  216. # so that:
  217. #
  218. # Person._validate_callbacks.empty? # => true
  219. #
  220. 1 def clear_validators!
  221. 658 reset_callbacks(:validate)
  222. 658 _validators.clear
  223. end
  224. # List all validators that are being used to validate a specific attribute.
  225. #
  226. # class Person
  227. # include ActiveModel::Validations
  228. #
  229. # attr_accessor :name , :age
  230. #
  231. # validates_presence_of :name
  232. # validates_inclusion_of :age, in: 0..99
  233. # end
  234. #
  235. # Person.validators_on(:name)
  236. # # => [
  237. # # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>,
  238. # # ]
  239. 1 def validators_on(*attributes)
  240. 7 attributes.flat_map do |attribute|
  241. 8 _validators[attribute.to_sym]
  242. end
  243. end
  244. # Returns +true+ if +attribute+ is an attribute method, +false+ otherwise.
  245. #
  246. # class Person
  247. # include ActiveModel::Validations
  248. #
  249. # attr_accessor :name
  250. # end
  251. #
  252. # User.attribute_method?(:name) # => true
  253. # User.attribute_method?(:age) # => false
  254. 1 def attribute_method?(attribute)
  255. 22 method_defined?(attribute)
  256. end
  257. # Copy validators on inheritance.
  258. 1 def inherited(base) #:nodoc:
  259. 158 dup = _validators.dup
  260. 158 base._validators = dup.each { |k, v| dup[k] = v.dup }
  261. 158 super
  262. end
  263. end
  264. # Clean the +Errors+ object if instance is duped.
  265. 1 def initialize_dup(other) #:nodoc:
  266. 2 @errors = nil
  267. 2 super
  268. end
  269. # Returns the +Errors+ object that holds all information about attribute
  270. # error messages.
  271. #
  272. # class Person
  273. # include ActiveModel::Validations
  274. #
  275. # attr_accessor :name
  276. # validates_presence_of :name
  277. # end
  278. #
  279. # person = Person.new
  280. # person.valid? # => false
  281. # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={name:["can't be blank"]}>
  282. 1 def errors
  283. 3292 @errors ||= Errors.new(self)
  284. end
  285. # Runs all the specified validations and returns +true+ if no errors were
  286. # added otherwise +false+.
  287. #
  288. # class Person
  289. # include ActiveModel::Validations
  290. #
  291. # attr_accessor :name
  292. # validates_presence_of :name
  293. # end
  294. #
  295. # person = Person.new
  296. # person.name = ''
  297. # person.valid? # => false
  298. # person.name = 'david'
  299. # person.valid? # => true
  300. #
  301. # Context can optionally be supplied to define which callbacks to test
  302. # against (the context is defined on the validations using <tt>:on</tt>).
  303. #
  304. # class Person
  305. # include ActiveModel::Validations
  306. #
  307. # attr_accessor :name
  308. # validates_presence_of :name, on: :new
  309. # end
  310. #
  311. # person = Person.new
  312. # person.valid? # => true
  313. # person.valid?(:new) # => false
  314. 1 def valid?(context = nil)
  315. 918 current_context, self.validation_context = validation_context, context
  316. 918 errors.clear
  317. 918 run_validations!
  318. ensure
  319. 918 self.validation_context = current_context
  320. end
  321. 1 alias_method :validate, :valid?
  322. # Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
  323. # added, +false+ otherwise.
  324. #
  325. # class Person
  326. # include ActiveModel::Validations
  327. #
  328. # attr_accessor :name
  329. # validates_presence_of :name
  330. # end
  331. #
  332. # person = Person.new
  333. # person.name = ''
  334. # person.invalid? # => true
  335. # person.name = 'david'
  336. # person.invalid? # => false
  337. #
  338. # Context can optionally be supplied to define which callbacks to test
  339. # against (the context is defined on the validations using <tt>:on</tt>).
  340. #
  341. # class Person
  342. # include ActiveModel::Validations
  343. #
  344. # attr_accessor :name
  345. # validates_presence_of :name, on: :new
  346. # end
  347. #
  348. # person = Person.new
  349. # person.invalid? # => false
  350. # person.invalid?(:new) # => true
  351. 1 def invalid?(context = nil)
  352. 358 !valid?(context)
  353. end
  354. # Runs all the validations within the specified context. Returns +true+ if
  355. # no errors are found, raises +ValidationError+ otherwise.
  356. #
  357. # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
  358. # some <tt>:on</tt> option will only run in the specified context.
  359. 1 def validate!(context = nil)
  360. 3 valid?(context) || raise_validation_error
  361. end
  362. # Hook method defining how an attribute value should be retrieved. By default
  363. # this is assumed to be an instance named after the attribute. Override this
  364. # method in subclasses should you need to retrieve the value for a given
  365. # attribute differently:
  366. #
  367. # class MyClass
  368. # include ActiveModel::Validations
  369. #
  370. # def initialize(data = {})
  371. # @data = data
  372. # end
  373. #
  374. # def read_attribute_for_validation(key)
  375. # @data[key]
  376. # end
  377. # end
  378. 1 alias :read_attribute_for_validation :send
  379. 1 private
  380. 1 def run_validations!
  381. 917 _run_validate_callbacks
  382. 910 errors.empty?
  383. end
  384. 1 def raise_validation_error # :doc:
  385. 2 raise(ValidationError.new(self))
  386. end
  387. end
  388. # = Active Model ValidationError
  389. #
  390. # Raised by <tt>validate!</tt> when the model is invalid. Use the
  391. # +model+ method to retrieve the record which did not validate.
  392. #
  393. # begin
  394. # complex_operation_that_internally_calls_validate!
  395. # rescue ActiveModel::ValidationError => invalid
  396. # puts invalid.model.errors
  397. # end
  398. 1 class ValidationError < StandardError
  399. 1 attr_reader :model
  400. 1 def initialize(model)
  401. 2 @model = model
  402. 2 errors = @model.errors.full_messages.join(", ")
  403. 2 super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid"))
  404. end
  405. end
  406. end
  407. 15 Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }

lib/active_model/validations/absence.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. # == \Active \Model Absence Validator
  5. 1 class AbsenceValidator < EachValidator #:nodoc:
  6. 1 def validate_each(record, attr_name, value)
  7. 13 record.errors.add(attr_name, :present, **options) if value.present?
  8. end
  9. end
  10. 1 module HelperMethods
  11. # Validates that the specified attributes are blank (as defined by
  12. # Object#present?). Happens by default on save.
  13. #
  14. # class Person < ActiveRecord::Base
  15. # validates_absence_of :first_name
  16. # end
  17. #
  18. # The first_name attribute must be in the object and it must be blank.
  19. #
  20. # Configuration options:
  21. # * <tt>:message</tt> - A custom error message (default is: "must be blank").
  22. #
  23. # There is also a list of default options supported by every validator:
  24. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
  25. # See <tt>ActiveModel::Validations#validates</tt> for more information
  26. 1 def validates_absence_of(*attr_names)
  27. 5 validates_with AbsenceValidator, _merge_attributes(attr_names)
  28. end
  29. end
  30. end
  31. end

lib/active_model/validations/acceptance.rb

97.96% lines covered

49 relevant lines. 48 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. 1 class AcceptanceValidator < EachValidator # :nodoc:
  5. 1 def initialize(options)
  6. 22 super({ allow_nil: true, accept: ["1", true] }.merge!(options))
  7. 22 setup!(options[:class])
  8. end
  9. 1 def validate_each(record, attribute, value)
  10. 20 unless acceptable_option?(value)
  11. 13 record.errors.add(attribute, :accepted, **options.except(:accept, :allow_nil))
  12. end
  13. end
  14. 1 private
  15. 1 def setup!(klass)
  16. 22 define_attributes = LazilyDefineAttributes.new(attributes)
  17. 22 klass.include(define_attributes) unless klass.included_modules.include?(define_attributes)
  18. end
  19. 1 def acceptable_option?(value)
  20. 20 Array(options[:accept]).include?(value)
  21. end
  22. 1 class LazilyDefineAttributes < Module
  23. 1 def initialize(attributes)
  24. 22 @attributes = attributes.map(&:to_s)
  25. end
  26. 1 def included(klass)
  27. 20 @lock = Mutex.new
  28. 20 mod = self
  29. 20 define_method(:respond_to_missing?) do |method_name, include_private = false|
  30. 4 mod.define_on(klass)
  31. 4 super(method_name, include_private) || mod.matches?(method_name)
  32. end
  33. 20 define_method(:method_missing) do |method_name, *args, &block|
  34. 7 mod.define_on(klass)
  35. 7 if mod.matches?(method_name)
  36. 7 send(method_name, *args, &block)
  37. else
  38. super(method_name, *args, &block)
  39. end
  40. end
  41. end
  42. 1 def matches?(method_name)
  43. 11 attr_name = method_name.to_s.chomp("=")
  44. 22 attributes.any? { |name| name == attr_name }
  45. end
  46. 1 def define_on(klass)
  47. 11 @lock&.synchronize do
  48. 11 return unless @lock
  49. 22 attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
  50. 22 attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
  51. 11 attr_reader(*attr_readers)
  52. 11 attr_writer(*attr_writers)
  53. 11 remove_method :respond_to_missing?
  54. 11 remove_method :method_missing
  55. 11 @lock = nil
  56. end
  57. end
  58. 1 def ==(other)
  59. 4 self.class == other.class && attributes == other.attributes
  60. end
  61. 1 protected
  62. 1 attr_reader :attributes
  63. end
  64. end
  65. 1 module HelperMethods
  66. # Encapsulates the pattern of wanting to validate the acceptance of a
  67. # terms of service check box (or similar agreement).
  68. #
  69. # class Person < ActiveRecord::Base
  70. # validates_acceptance_of :terms_of_service
  71. # validates_acceptance_of :eula, message: 'must be abided'
  72. # end
  73. #
  74. # If the database column does not exist, the +terms_of_service+ attribute
  75. # is entirely virtual. This check is performed only if +terms_of_service+
  76. # is not +nil+ and by default on save.
  77. #
  78. # Configuration options:
  79. # * <tt>:message</tt> - A custom error message (default is: "must be
  80. # accepted").
  81. # * <tt>:accept</tt> - Specifies a value that is considered accepted.
  82. # Also accepts an array of possible values. The default value is
  83. # an array ["1", true], which makes it easy to relate to an HTML
  84. # checkbox. This should be set to, or include, +true+ if you are validating
  85. # a database column, since the attribute is typecast from "1" to +true+
  86. # before validation.
  87. #
  88. # There is also a list of default options supported by every validator:
  89. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
  90. # See <tt>ActiveModel::Validations#validates</tt> for more information.
  91. 1 def validates_acceptance_of(*attr_names)
  92. 22 validates_with AcceptanceValidator, _merge_attributes(attr_names)
  93. end
  94. end
  95. end
  96. end

lib/active_model/validations/callbacks.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. # == Active \Model \Validation \Callbacks
  5. #
  6. # Provides an interface for any class to have +before_validation+ and
  7. # +after_validation+ callbacks.
  8. #
  9. # First, include ActiveModel::Validations::Callbacks from the class you are
  10. # creating:
  11. #
  12. # class MyModel
  13. # include ActiveModel::Validations::Callbacks
  14. #
  15. # before_validation :do_stuff_before_validation
  16. # after_validation :do_stuff_after_validation
  17. # end
  18. #
  19. # Like other <tt>before_*</tt> callbacks if +before_validation+ throws
  20. # +:abort+ then <tt>valid?</tt> will not be called.
  21. 1 module Callbacks
  22. 1 extend ActiveSupport::Concern
  23. 1 included do
  24. 2 include ActiveSupport::Callbacks
  25. 2 define_callbacks :validation,
  26. skip_after_callbacks_if_terminated: true,
  27. scope: [:kind, :name]
  28. end
  29. 1 module ClassMethods
  30. # Defines a callback that will get called right before validation.
  31. #
  32. # class Person
  33. # include ActiveModel::Validations
  34. # include ActiveModel::Validations::Callbacks
  35. #
  36. # attr_accessor :name
  37. #
  38. # validates_length_of :name, maximum: 6
  39. #
  40. # before_validation :remove_whitespaces
  41. #
  42. # private
  43. #
  44. # def remove_whitespaces
  45. # name.strip!
  46. # end
  47. # end
  48. #
  49. # person = Person.new
  50. # person.name = ' bob '
  51. # person.valid? # => true
  52. # person.name # => "bob"
  53. 1 def before_validation(*args, &block)
  54. 14 options = args.extract_options!
  55. 14 if options.key?(:on)
  56. 3 options = options.dup
  57. 3 options[:on] = Array(options[:on])
  58. 3 options[:if] = Array(options[:if])
  59. 3 options[:if].unshift ->(o) {
  60. 13 !(options[:on] & Array(o.validation_context)).empty?
  61. }
  62. end
  63. 14 set_callback(:validation, :before, *args, options, &block)
  64. end
  65. # Defines a callback that will get called right after validation.
  66. #
  67. # class Person
  68. # include ActiveModel::Validations
  69. # include ActiveModel::Validations::Callbacks
  70. #
  71. # attr_accessor :name, :status
  72. #
  73. # validates_presence_of :name
  74. #
  75. # after_validation :set_status
  76. #
  77. # private
  78. #
  79. # def set_status
  80. # self.status = errors.empty?
  81. # end
  82. # end
  83. #
  84. # person = Person.new
  85. # person.name = ''
  86. # person.valid? # => false
  87. # person.status # => false
  88. # person.name = 'bob'
  89. # person.valid? # => true
  90. # person.status # => true
  91. 1 def after_validation(*args, &block)
  92. 10 options = args.extract_options!
  93. 10 options = options.dup
  94. 10 options[:prepend] = true
  95. 10 if options.key?(:on)
  96. 3 options[:on] = Array(options[:on])
  97. 3 options[:if] = Array(options[:if])
  98. 3 options[:if].unshift ->(o) {
  99. 13 !(options[:on] & Array(o.validation_context)).empty?
  100. }
  101. end
  102. 10 set_callback(:validation, :after, *args, options, &block)
  103. end
  104. end
  105. 1 private
  106. # Overwrite run validations to include callbacks.
  107. 1 def run_validations!
  108. 1447 _run_validation_callbacks { super }
  109. end
  110. end
  111. end
  112. end

lib/active_model/validations/clusivity.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/range"
  3. 1 module ActiveModel
  4. 1 module Validations
  5. 1 module Clusivity #:nodoc:
  6. 1 ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \
  7. "and must be supplied as the :in (or :within) option of the configuration hash"
  8. 1 def check_validity!
  9. 52 unless delimiter.respond_to?(:include?) || delimiter.respond_to?(:call) || delimiter.respond_to?(:to_sym)
  10. 2 raise ArgumentError, ERROR_MESSAGE
  11. end
  12. end
  13. 1 private
  14. 1 def include?(record, value)
  15. 86 members = if delimiter.respond_to?(:call)
  16. 4 delimiter.call(record)
  17. 82 elsif delimiter.respond_to?(:to_sym)
  18. 4 record.send(delimiter)
  19. else
  20. 78 delimiter
  21. end
  22. 86 if value.is_a?(Array)
  23. 5 value.all? { |v| members.send(inclusion_method(members), v) }
  24. else
  25. 84 members.send(inclusion_method(members), value)
  26. end
  27. end
  28. 1 def delimiter
  29. 316 @delimiter ||= options[:in] || options[:within]
  30. end
  31. # After Ruby 2.2, <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
  32. # possible values in the range for equality, which is slower but more accurate.
  33. # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
  34. # endpoints, which is fast but is only accurate on Numeric, Time, Date,
  35. # or DateTime ranges.
  36. 1 def inclusion_method(enumerable)
  37. 87 if enumerable.is_a? Range
  38. 29 case enumerable.first
  39. when Numeric, Time, DateTime, Date
  40. 21 :cover?
  41. else
  42. 8 :include?
  43. end
  44. else
  45. 58 :include?
  46. end
  47. end
  48. end
  49. end
  50. end

lib/active_model/validations/confirmation.rb

100.0% lines covered

24 relevant lines. 24 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. 1 class ConfirmationValidator < EachValidator # :nodoc:
  5. 1 def initialize(options)
  6. 19 super({ case_sensitive: true }.merge!(options))
  7. 19 setup!(options[:class])
  8. end
  9. 1 def validate_each(record, attribute, value)
  10. 35 unless (confirmed = record.send("#{attribute}_confirmation")).nil?
  11. 30 unless confirmation_value_equal?(record, attribute, value, confirmed)
  12. 19 human_attribute_name = record.class.human_attribute_name(attribute)
  13. 19 record.errors.add(:"#{attribute}_confirmation", :confirmation, **options.except(:case_sensitive).merge!(attribute: human_attribute_name))
  14. end
  15. end
  16. end
  17. 1 private
  18. 1 def setup!(klass)
  19. 19 klass.attr_reader(*attributes.map do |attribute|
  20. 19 :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
  21. end.compact)
  22. 19 klass.attr_writer(*attributes.map do |attribute|
  23. 19 :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
  24. end.compact)
  25. end
  26. 1 def confirmation_value_equal?(record, attribute, value, confirmed)
  27. 30 if !options[:case_sensitive] && value.is_a?(String)
  28. 1 value.casecmp(confirmed) == 0
  29. else
  30. 29 value == confirmed
  31. end
  32. end
  33. end
  34. 1 module HelperMethods
  35. # Encapsulates the pattern of wanting to validate a password or email
  36. # address field with a confirmation.
  37. #
  38. # Model:
  39. # class Person < ActiveRecord::Base
  40. # validates_confirmation_of :user_name, :password
  41. # validates_confirmation_of :email_address,
  42. # message: 'should match confirmation'
  43. # end
  44. #
  45. # View:
  46. # <%= password_field "person", "password" %>
  47. # <%= password_field "person", "password_confirmation" %>
  48. #
  49. # The added +password_confirmation+ attribute is virtual; it exists only
  50. # as an in-memory attribute for validating the password. To achieve this,
  51. # the validation adds accessors to the model for the confirmation
  52. # attribute.
  53. #
  54. # NOTE: This check is performed only if +password_confirmation+ is not
  55. # +nil+. To require confirmation, make sure to add a presence check for
  56. # the confirmation attribute:
  57. #
  58. # validates_presence_of :password_confirmation, if: :password_changed?
  59. #
  60. # Configuration options:
  61. # * <tt>:message</tt> - A custom error message (default is: "doesn't match
  62. # <tt>%{translated_attribute_name}</tt>").
  63. # * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
  64. # non-text columns (+true+ by default).
  65. #
  66. # There is also a list of default options supported by every validator:
  67. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
  68. # See <tt>ActiveModel::Validations#validates</tt> for more information
  69. 1 def validates_confirmation_of(*attr_names)
  70. 18 validates_with ConfirmationValidator, _merge_attributes(attr_names)
  71. end
  72. end
  73. end
  74. end

lib/active_model/validations/exclusion.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/validations/clusivity"
  3. 1 module ActiveModel
  4. 1 module Validations
  5. 1 class ExclusionValidator < EachValidator # :nodoc:
  6. 1 include Clusivity
  7. 1 def validate_each(record, attribute, value)
  8. 29 if include?(record, value)
  9. 22 record.errors.add(attribute, :exclusion, **options.except(:in, :within).merge!(value: value))
  10. end
  11. end
  12. end
  13. 1 module HelperMethods
  14. # Validates that the value of the specified attribute is not in a
  15. # particular enumerable object.
  16. #
  17. # class Person < ActiveRecord::Base
  18. # validates_exclusion_of :username, in: %w( admin superuser ), message: "You don't belong here"
  19. # validates_exclusion_of :age, in: 30..60, message: 'This site is only for under 30 and over 60'
  20. # validates_exclusion_of :format, in: %w( mov avi ), message: "extension %{value} is not allowed"
  21. # validates_exclusion_of :password, in: ->(person) { [person.username, person.first_name] },
  22. # message: 'should not be the same as your username or first name'
  23. # validates_exclusion_of :karma, in: :reserved_karmas
  24. # end
  25. #
  26. # Configuration options:
  27. # * <tt>:in</tt> - An enumerable object of items that the value shouldn't
  28. # be part of. This can be supplied as a proc, lambda or symbol which returns an
  29. # enumerable. If the enumerable is a numerical, time or datetime range the test
  30. # is performed with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When
  31. # using a proc or lambda the instance under validation is passed as an argument.
  32. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
  33. # <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>.
  34. # * <tt>:message</tt> - Specifies a custom error message (default is: "is
  35. # reserved").
  36. #
  37. # There is also a list of default options supported by every validator:
  38. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
  39. # See <tt>ActiveModel::Validations#validates</tt> for more information
  40. 1 def validates_exclusion_of(*attr_names)
  41. 21 validates_with ExclusionValidator, _merge_attributes(attr_names)
  42. end
  43. end
  44. end
  45. end

lib/active_model/validations/format.rb

100.0% lines covered

34 relevant lines. 34 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. 1 class FormatValidator < EachValidator # :nodoc:
  5. 1 def validate_each(record, attribute, value)
  6. 37 if options[:with]
  7. 33 regexp = option_call(record, :with)
  8. 33 record_error(record, attribute, :with, value) unless regexp.match?(value.to_s)
  9. 4 elsif options[:without]
  10. 4 regexp = option_call(record, :without)
  11. 4 record_error(record, attribute, :without, value) if regexp.match?(value.to_s)
  12. end
  13. end
  14. 1 def check_validity!
  15. 25 unless options.include?(:with) ^ options.include?(:without) # ^ == xor, or "exclusive or"
  16. 3 raise ArgumentError, "Either :with or :without must be supplied (but not both)"
  17. end
  18. 22 check_options_validity :with
  19. 20 check_options_validity :without
  20. end
  21. 1 private
  22. 1 def option_call(record, name)
  23. 37 option = options[name]
  24. 37 option.respond_to?(:call) ? option.call(record) : option
  25. end
  26. 1 def record_error(record, attribute, name, value)
  27. 21 record.errors.add(attribute, :invalid, **options.except(name).merge!(value: value))
  28. end
  29. 1 def check_options_validity(name)
  30. 42 if option = options[name]
  31. 22 if option.is_a?(Regexp)
  32. 18 if options[:multiline] != true && regexp_using_multiline_anchors?(option)
  33. 1 raise ArgumentError, "The provided regular expression is using multiline anchors (^ or $), " \
  34. "which may present a security risk. Did you mean to use \\A and \\z, or forgot to add the " \
  35. ":multiline => true option?"
  36. end
  37. 4 elsif !option.respond_to?(:call)
  38. 2 raise ArgumentError, "A regular expression or a proc or lambda must be supplied as :#{name}"
  39. end
  40. end
  41. end
  42. 1 def regexp_using_multiline_anchors?(regexp)
  43. 17 source = regexp.source
  44. 17 source.start_with?("^") || (source.end_with?("$") && !source.end_with?("\\$"))
  45. end
  46. end
  47. 1 module HelperMethods
  48. # Validates whether the value of the specified attribute is of the correct
  49. # form, going by the regular expression provided. You can require that the
  50. # attribute matches the regular expression:
  51. #
  52. # class Person < ActiveRecord::Base
  53. # validates_format_of :email, with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create
  54. # end
  55. #
  56. # Alternatively, you can require that the specified attribute does _not_
  57. # match the regular expression:
  58. #
  59. # class Person < ActiveRecord::Base
  60. # validates_format_of :email, without: /NOSPAM/
  61. # end
  62. #
  63. # You can also provide a proc or lambda which will determine the regular
  64. # expression that will be used to validate the attribute.
  65. #
  66. # class Person < ActiveRecord::Base
  67. # # Admin can have number as a first letter in their screen name
  68. # validates_format_of :screen_name,
  69. # with: ->(person) { person.admin? ? /\A[a-z0-9][a-z0-9_\-]*\z/i : /\A[a-z][a-z0-9_\-]*\z/i }
  70. # end
  71. #
  72. # Note: use <tt>\A</tt> and <tt>\z</tt> to match the start and end of the
  73. # string, <tt>^</tt> and <tt>$</tt> match the start/end of a line.
  74. #
  75. # Due to frequent misuse of <tt>^</tt> and <tt>$</tt>, you need to pass
  76. # the <tt>multiline: true</tt> option in case you use any of these two
  77. # anchors in the provided regular expression. In most cases, you should be
  78. # using <tt>\A</tt> and <tt>\z</tt>.
  79. #
  80. # You must pass either <tt>:with</tt> or <tt>:without</tt> as an option.
  81. # In addition, both must be a regular expression or a proc or lambda, or
  82. # else an exception will be raised.
  83. #
  84. # Configuration options:
  85. # * <tt>:message</tt> - A custom error message (default is: "is invalid").
  86. # * <tt>:with</tt> - Regular expression that if the attribute matches will
  87. # result in a successful validation. This can be provided as a proc or
  88. # lambda returning regular expression which will be called at runtime.
  89. # * <tt>:without</tt> - Regular expression that if the attribute does not
  90. # match will result in a successful validation. This can be provided as
  91. # a proc or lambda returning regular expression which will be called at
  92. # runtime.
  93. # * <tt>:multiline</tt> - Set to true if your regular expression contains
  94. # anchors that match the beginning or end of lines as opposed to the
  95. # beginning or end of the string. These anchors are <tt>^</tt> and <tt>$</tt>.
  96. #
  97. # There is also a list of default options supported by every validator:
  98. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
  99. # See <tt>ActiveModel::Validations#validates</tt> for more information
  100. 1 def validates_format_of(*attr_names)
  101. 23 validates_with FormatValidator, _merge_attributes(attr_names)
  102. end
  103. end
  104. end
  105. end

lib/active_model/validations/helper_methods.rb

100.0% lines covered

9 relevant lines. 9 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. 1 module HelperMethods # :nodoc:
  5. 1 private
  6. 1 def _merge_attributes(attr_names)
  7. 323 options = attr_names.extract_options!.symbolize_keys
  8. 323 attr_names.flatten!
  9. 323 options[:attributes] = attr_names
  10. 323 options
  11. end
  12. end
  13. end
  14. end

lib/active_model/validations/inclusion.rb

100.0% lines covered

11 relevant lines. 11 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_model/validations/clusivity"
  3. 1 module ActiveModel
  4. 1 module Validations
  5. 1 class InclusionValidator < EachValidator # :nodoc:
  6. 1 include Clusivity
  7. 1 def validate_each(record, attribute, value)
  8. 57 unless include?(record, value)
  9. 35 record.errors.add(attribute, :inclusion, **options.except(:in, :within).merge!(value: value))
  10. end
  11. end
  12. end
  13. 1 module HelperMethods
  14. # Validates whether the value of the specified attribute is available in a
  15. # particular enumerable object.
  16. #
  17. # class Person < ActiveRecord::Base
  18. # validates_inclusion_of :role, in: %w( admin contributor )
  19. # validates_inclusion_of :age, in: 0..99
  20. # validates_inclusion_of :format, in: %w( jpg gif png ), message: "extension %{value} is not included in the list"
  21. # validates_inclusion_of :states, in: ->(person) { STATES[person.country] }
  22. # validates_inclusion_of :karma, in: :available_karmas
  23. # end
  24. #
  25. # Configuration options:
  26. # * <tt>:in</tt> - An enumerable object of available items. This can be
  27. # supplied as a proc, lambda or symbol which returns an enumerable. If the
  28. # enumerable is a numerical, time or datetime range the test is performed
  29. # with <tt>Range#cover?</tt>, otherwise with <tt>include?</tt>. When using
  30. # a proc or lambda the instance under validation is passed as an argument.
  31. # * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
  32. # * <tt>:message</tt> - Specifies a custom error message (default is: "is
  33. # not included in the list").
  34. #
  35. # There is also a list of default options supported by every validator:
  36. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
  37. # See <tt>ActiveModel::Validations#validates</tt> for more information
  38. 1 def validates_inclusion_of(*attr_names)
  39. 30 validates_with InclusionValidator, _merge_attributes(attr_names)
  40. end
  41. end
  42. end
  43. end

lib/active_model/validations/length.rb

95.24% lines covered

42 relevant lines. 40 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. 1 class LengthValidator < EachValidator # :nodoc:
  5. 1 MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
  6. 1 CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
  7. 1 RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :too_short, :too_long]
  8. 1 def initialize(options)
  9. 100 if range = (options.delete(:in) || options.delete(:within))
  10. 29 raise ArgumentError, ":in and :within must be a Range" unless range.is_a?(Range)
  11. 27 options[:minimum], options[:maximum] = range.min, range.max
  12. end
  13. 98 if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?
  14. 1 options[:minimum] = 1
  15. end
  16. 98 super
  17. end
  18. 1 def check_validity!
  19. 98 keys = CHECKS.keys & options.keys
  20. 98 if keys.empty?
  21. raise ArgumentError, "Range unspecified. Specify the :in, :within, :maximum, :minimum, or :is option."
  22. end
  23. 98 keys.each do |key|
  24. 128 value = options[key]
  25. 128 unless (value.is_a?(Integer) && value >= 0) || value == Float::INFINITY || value.is_a?(Symbol) || value.is_a?(Proc)
  26. 4 raise ArgumentError, ":#{key} must be a non-negative Integer, Infinity, Symbol, or Proc"
  27. end
  28. end
  29. end
  30. 1 def validate_each(record, attribute, value)
  31. 143 value_length = value.respond_to?(:length) ? value.length : value.to_s.length
  32. 143 errors_options = options.except(*RESERVED_OPTIONS)
  33. 143 CHECKS.each do |key, validity_check|
  34. 429 next unless check_value = options[key]
  35. 191 if !value.nil? || skip_nil_check?(key)
  36. 160 case check_value
  37. when Proc
  38. 6 check_value = check_value.call(record)
  39. when Symbol
  40. check_value = record.send(check_value)
  41. end
  42. 160 next if value_length.send(validity_check, check_value)
  43. end
  44. 84 errors_options[:count] = check_value
  45. 84 default_message = options[MESSAGES[key]]
  46. 84 errors_options[:message] ||= default_message if default_message
  47. 84 record.errors.add(attribute, MESSAGES[key], **errors_options)
  48. end
  49. end
  50. 1 private
  51. 1 def skip_nil_check?(key)
  52. 54 key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
  53. end
  54. end
  55. 1 module HelperMethods
  56. # Validates that the specified attributes match the length restrictions
  57. # supplied. Only one constraint option can be used at a time apart from
  58. # +:minimum+ and +:maximum+ that can be combined together:
  59. #
  60. # class Person < ActiveRecord::Base
  61. # validates_length_of :first_name, maximum: 30
  62. # validates_length_of :last_name, maximum: 30, message: "less than 30 if you don't mind"
  63. # validates_length_of :fax, in: 7..32, allow_nil: true
  64. # validates_length_of :phone, in: 7..32, allow_blank: true
  65. # validates_length_of :user_name, within: 6..20, too_long: 'pick a shorter name', too_short: 'pick a longer name'
  66. # validates_length_of :zip_code, minimum: 5, too_short: 'please enter at least 5 characters'
  67. # validates_length_of :smurf_leader, is: 4, message: "papa is spelled with 4 characters... don't play me."
  68. # validates_length_of :words_in_essay, minimum: 100, too_short: 'Your essay must be at least 100 words.'
  69. #
  70. # private
  71. #
  72. # def words_in_essay
  73. # essay.scan(/\w+/)
  74. # end
  75. # end
  76. #
  77. # Constraint options:
  78. #
  79. # * <tt>:minimum</tt> - The minimum size of the attribute.
  80. # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by
  81. # default if not used with +:minimum+.
  82. # * <tt>:is</tt> - The exact size of the attribute.
  83. # * <tt>:within</tt> - A range specifying the minimum and maximum size of
  84. # the attribute.
  85. # * <tt>:in</tt> - A synonym (or alias) for <tt>:within</tt>.
  86. #
  87. # Other options:
  88. #
  89. # * <tt>:allow_nil</tt> - Attribute may be +nil+; skip validation.
  90. # * <tt>:allow_blank</tt> - Attribute may be blank; skip validation.
  91. # * <tt>:too_long</tt> - The error message if the attribute goes over the
  92. # maximum (default is: "is too long (maximum is %{count} characters)").
  93. # * <tt>:too_short</tt> - The error message if the attribute goes under the
  94. # minimum (default is: "is too short (minimum is %{count} characters)").
  95. # * <tt>:wrong_length</tt> - The error message if using the <tt>:is</tt>
  96. # method and the attribute is the wrong size (default is: "is the wrong
  97. # length (should be %{count} characters)").
  98. # * <tt>:message</tt> - The error message to use for a <tt>:minimum</tt>,
  99. # <tt>:maximum</tt>, or <tt>:is</tt> violation. An alias of the appropriate
  100. # <tt>too_long</tt>/<tt>too_short</tt>/<tt>wrong_length</tt> message.
  101. #
  102. # There is also a list of default options supported by every validator:
  103. # +:if+, +:unless+, +:on+ and +:strict+.
  104. # See <tt>ActiveModel::Validations#validates</tt> for more information
  105. 1 def validates_length_of(*attr_names)
  106. 95 validates_with LengthValidator, _merge_attributes(attr_names)
  107. end
  108. 1 alias_method :validates_size_of, :validates_length_of
  109. end
  110. end
  111. end

lib/active_model/validations/numericality.rb

94.74% lines covered

76 relevant lines. 72 lines covered and 4 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "bigdecimal/util"
  3. 1 module ActiveModel
  4. 1 module Validations
  5. 1 class NumericalityValidator < EachValidator # :nodoc:
  6. 1 CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
  7. equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
  8. odd: :odd?, even: :even?, other_than: :!= }.freeze
  9. 1 RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
  10. 1 INTEGER_REGEX = /\A[+-]?\d+\z/
  11. 1 HEXADECIMAL_REGEX = /\A[+-]?0[xX]/
  12. 1 def check_validity!
  13. 76 keys = CHECKS.keys - [:odd, :even]
  14. 76 options.slice(*keys).each do |option, value|
  15. 38 unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
  16. 5 raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
  17. end
  18. end
  19. end
  20. 1 def validate_each(record, attr_name, value, precision: Float::DIG, scale: nil)
  21. 482 unless is_number?(value, precision, scale)
  22. 132 record.errors.add(attr_name, :not_a_number, **filtered_options(value))
  23. 132 return
  24. end
  25. 350 if allow_only_integer?(record) && !is_integer?(value)
  26. 46 record.errors.add(attr_name, :not_an_integer, **filtered_options(value))
  27. 46 return
  28. end
  29. 304 value = parse_as_number(value, precision, scale)
  30. 304 options.slice(*CHECKS.keys).each do |option, option_value|
  31. 136 case option
  32. when :odd, :even
  33. 20 unless value.to_i.send(CHECKS[option])
  34. 14 record.errors.add(attr_name, option, **filtered_options(value))
  35. end
  36. else
  37. 116 case option_value
  38. when Proc
  39. 4 option_value = option_value.call(record)
  40. when Symbol
  41. 3 option_value = record.send(option_value)
  42. end
  43. 116 option_value = parse_as_number(option_value, precision, scale)
  44. 116 unless value.send(CHECKS[option], option_value)
  45. 61 record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
  46. end
  47. end
  48. end
  49. end
  50. 1 private
  51. 1 def parse_as_number(raw_value, precision, scale)
  52. 902 if raw_value.is_a?(Float)
  53. 102 parse_float(raw_value, precision, scale)
  54. 800 elsif raw_value.is_a?(Numeric)
  55. 328 raw_value
  56. 472 elsif is_integer?(raw_value)
  57. 162 raw_value.to_i
  58. 310 elsif !is_hexadecimal_literal?(raw_value)
  59. 275 parse_float(Kernel.Float(raw_value), precision, scale)
  60. end
  61. end
  62. 1 def parse_float(raw_value, precision, scale)
  63. 280 (scale ? raw_value.truncate(scale) : raw_value).to_d(precision)
  64. end
  65. 1 def is_number?(raw_value, precision, scale)
  66. 482 !parse_as_number(raw_value, precision, scale).nil?
  67. rescue ArgumentError, TypeError
  68. 97 false
  69. end
  70. 1 def is_integer?(raw_value)
  71. 556 INTEGER_REGEX.match?(raw_value.to_s)
  72. end
  73. 1 def is_hexadecimal_literal?(raw_value)
  74. 310 HEXADECIMAL_REGEX.match?(raw_value.to_s)
  75. end
  76. 1 def filtered_options(value)
  77. 253 filtered = options.except(*RESERVED_OPTIONS)
  78. 253 filtered[:value] = value
  79. 253 filtered
  80. end
  81. 1 def allow_only_integer?(record)
  82. 350 case options[:only_integer]
  83. when Symbol
  84. 30 record.send(options[:only_integer])
  85. when Proc
  86. 30 options[:only_integer].call(record)
  87. else
  88. 290 options[:only_integer]
  89. end
  90. end
  91. 1 def read_attribute_for_validation(record, attr_name)
  92. 488 return super if record_attribute_changed_in_place?(record, attr_name)
  93. 488 came_from_user = :"#{attr_name}_came_from_user?"
  94. 488 if record.respond_to?(came_from_user)
  95. if record.public_send(came_from_user)
  96. raw_value = record.read_attribute_before_type_cast(attr_name)
  97. elsif record.respond_to?(:read_attribute)
  98. raw_value = record.read_attribute(attr_name)
  99. end
  100. else
  101. 488 before_type_cast = :"#{attr_name}_before_type_cast"
  102. 488 if record.respond_to?(before_type_cast)
  103. 2 raw_value = record.public_send(before_type_cast)
  104. end
  105. end
  106. 488 raw_value || super
  107. end
  108. 1 def record_attribute_changed_in_place?(record, attr_name)
  109. 488 record.respond_to?(:attribute_changed_in_place?) &&
  110. record.attribute_changed_in_place?(attr_name.to_s)
  111. end
  112. end
  113. 1 module HelperMethods
  114. # Validates whether the value of the specified attribute is numeric by
  115. # trying to convert it to a float with Kernel.Float (if <tt>only_integer</tt>
  116. # is +false+) or applying it to the regular expression <tt>/\A[\+\-]?\d+\z/</tt>
  117. # (if <tt>only_integer</tt> is set to +true+). Precision of Kernel.Float values
  118. # are guaranteed up to 15 digits.
  119. #
  120. # class Person < ActiveRecord::Base
  121. # validates_numericality_of :value, on: :create
  122. # end
  123. #
  124. # Configuration options:
  125. # * <tt>:message</tt> - A custom error message (default is: "is not a number").
  126. # * <tt>:only_integer</tt> - Specifies whether the value has to be an
  127. # integer, e.g. an integral value (default is +false+).
  128. # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+ (default is
  129. # +false+). Notice that for Integer and Float columns empty strings are
  130. # converted to +nil+.
  131. # * <tt>:greater_than</tt> - Specifies the value must be greater than the
  132. # supplied value.
  133. # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
  134. # greater than or equal the supplied value.
  135. # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
  136. # value.
  137. # * <tt>:less_than</tt> - Specifies the value must be less than the
  138. # supplied value.
  139. # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
  140. # than or equal the supplied value.
  141. # * <tt>:other_than</tt> - Specifies the value must be other than the
  142. # supplied value.
  143. # * <tt>:odd</tt> - Specifies the value must be an odd number.
  144. # * <tt>:even</tt> - Specifies the value must be an even number.
  145. #
  146. # There is also a list of default options supported by every validator:
  147. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
  148. # See <tt>ActiveModel::Validations#validates</tt> for more information
  149. #
  150. # The following checks can also be supplied with a proc or a symbol which
  151. # corresponds to a method:
  152. #
  153. # * <tt>:greater_than</tt>
  154. # * <tt>:greater_than_or_equal_to</tt>
  155. # * <tt>:equal_to</tt>
  156. # * <tt>:less_than</tt>
  157. # * <tt>:less_than_or_equal_to</tt>
  158. # * <tt>:only_integer</tt>
  159. #
  160. # For example:
  161. #
  162. # class Person < ActiveRecord::Base
  163. # validates_numericality_of :width, less_than: ->(person) { person.height }
  164. # validates_numericality_of :width, greater_than: :minimum_weight
  165. # end
  166. 1 def validates_numericality_of(*attr_names)
  167. 73 validates_with NumericalityValidator, _merge_attributes(attr_names)
  168. end
  169. end
  170. end
  171. end

lib/active_model/validations/presence.rb

100.0% lines covered

8 relevant lines. 8 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 module ActiveModel
  3. 1 module Validations
  4. 1 class PresenceValidator < EachValidator # :nodoc:
  5. 1 def validate_each(record, attr_name, value)
  6. 65 record.errors.add(attr_name, :blank, **options) if value.blank?
  7. end
  8. end
  9. 1 module HelperMethods
  10. # Validates that the specified attributes are not blank (as defined by
  11. # Object#blank?). Happens by default on save.
  12. #
  13. # class Person < ActiveRecord::Base
  14. # validates_presence_of :first_name
  15. # end
  16. #
  17. # The first_name attribute must be in the object and it cannot be blank.
  18. #
  19. # If you want to validate the presence of a boolean field (where the real
  20. # values are +true+ and +false+), you will want to use
  21. # <tt>validates_inclusion_of :field_name, in: [true, false]</tt>.
  22. #
  23. # This is due to the way Object#blank? handles boolean values:
  24. # <tt>false.blank? # => true</tt>.
  25. #
  26. # Configuration options:
  27. # * <tt>:message</tt> - A custom error message (default is: "can't be blank").
  28. #
  29. # There is also a list of default options supported by every validator:
  30. # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+.
  31. # See <tt>ActiveModel::Validations#validates</tt> for more information
  32. 1 def validates_presence_of(*attr_names)
  33. 34 validates_with PresenceValidator, _merge_attributes(attr_names)
  34. end
  35. end
  36. end
  37. end

lib/active_model/validations/validates.rb

100.0% lines covered

30 relevant lines. 30 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/hash/slice"
  3. 1 module ActiveModel
  4. 1 module Validations
  5. 1 module ClassMethods
  6. # This method is a shortcut to all default validators and any custom
  7. # validator classes ending in 'Validator'. Note that Rails default
  8. # validators can be overridden inside specific classes by creating
  9. # custom validator classes in their place such as PresenceValidator.
  10. #
  11. # Examples of using the default rails validators:
  12. #
  13. # validates :username, absence: true
  14. # validates :terms, acceptance: true
  15. # validates :password, confirmation: true
  16. # validates :username, exclusion: { in: %w(admin superuser) }
  17. # validates :email, format: { with: /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, on: :create }
  18. # validates :age, inclusion: { in: 0..9 }
  19. # validates :first_name, length: { maximum: 30 }
  20. # validates :age, numericality: true
  21. # validates :username, presence: true
  22. #
  23. # The power of the +validates+ method comes when using custom validators
  24. # and default validators in one call for a given attribute.
  25. #
  26. # class EmailValidator < ActiveModel::EachValidator
  27. # def validate_each(record, attribute, value)
  28. # record.errors.add attribute, (options[:message] || "is not an email") unless
  29. # /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i.match?(value)
  30. # end
  31. # end
  32. #
  33. # class Person
  34. # include ActiveModel::Validations
  35. # attr_accessor :name, :email
  36. #
  37. # validates :name, presence: true, length: { maximum: 100 }
  38. # validates :email, presence: true, email: true
  39. # end
  40. #
  41. # Validator classes may also exist within the class being validated
  42. # allowing custom modules of validators to be included as needed.
  43. #
  44. # class Film
  45. # include ActiveModel::Validations
  46. #
  47. # class TitleValidator < ActiveModel::EachValidator
  48. # def validate_each(record, attribute, value)
  49. # record.errors.add attribute, "must start with 'the'" unless /\Athe/i.match?(value)
  50. # end
  51. # end
  52. #
  53. # validates :name, title: true
  54. # end
  55. #
  56. # Additionally validator classes may be in another namespace and still
  57. # used within any class.
  58. #
  59. # validates :name, :'film/title' => true
  60. #
  61. # The validators hash can also handle regular expressions, ranges, arrays
  62. # and strings in shortcut form.
  63. #
  64. # validates :email, format: /@/
  65. # validates :role, inclusion: %w(admin contributor)
  66. # validates :password, length: 6..20
  67. #
  68. # When using shortcut form, ranges and arrays are passed to your
  69. # validator's initializer as <tt>options[:in]</tt> while other types
  70. # including regular expressions and strings are passed as <tt>options[:with]</tt>.
  71. #
  72. # There is also a list of options that could be used along with validators:
  73. #
  74. # * <tt>:on</tt> - Specifies the contexts where this validation is active.
  75. # Runs in all validation contexts by default +nil+. You can pass a symbol
  76. # or an array of symbols. (e.g. <tt>on: :create</tt> or
  77. # <tt>on: :custom_validation_context</tt> or
  78. # <tt>on: [:create, :custom_validation_context]</tt>)
  79. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
  80. # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
  81. # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
  82. # proc or string should return or evaluate to a +true+ or +false+ value.
  83. # * <tt>:unless</tt> - Specifies a method, proc or string to call to determine
  84. # if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
  85. # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
  86. # method, proc or string should return or evaluate to a +true+ or
  87. # +false+ value.
  88. # * <tt>:allow_nil</tt> - Skip validation if the attribute is +nil+.
  89. # * <tt>:allow_blank</tt> - Skip validation if the attribute is blank.
  90. # * <tt>:strict</tt> - If the <tt>:strict</tt> option is set to true
  91. # will raise ActiveModel::StrictValidationFailed instead of adding the error.
  92. # <tt>:strict</tt> option can also be set to any other exception.
  93. #
  94. # Example:
  95. #
  96. # validates :password, presence: true, confirmation: true, if: :password_required?
  97. # validates :token, length: 24, strict: TokenLengthException
  98. #
  99. #
  100. # Finally, the options +:if+, +:unless+, +:on+, +:allow_blank+, +:allow_nil+, +:strict+
  101. # and +:message+ can be given to one specific validator, as a hash:
  102. #
  103. # validates :password, presence: { if: :password_required?, message: 'is forgotten.' }, confirmation: true
  104. 1 def validates(*attributes)
  105. 37 defaults = attributes.extract_options!.dup
  106. 37 validations = defaults.slice!(*_validates_default_keys)
  107. 37 raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
  108. 37 raise ArgumentError, "You need to supply at least one validation" if validations.empty?
  109. 37 defaults[:attributes] = attributes
  110. 37 validations.each do |key, options|
  111. 43 key = "#{key.to_s.camelize}Validator"
  112. 43 begin
  113. 43 validator = key.include?("::") ? key.constantize : const_get(key)
  114. rescue NameError
  115. 2 raise ArgumentError, "Unknown validator: '#{key}'"
  116. end
  117. 41 next unless options
  118. 40 validates_with(validator, defaults.merge(_parse_validates_options(options)))
  119. end
  120. end
  121. # This method is used to define validations that cannot be corrected by end
  122. # users and are considered exceptional. So each validator defined with bang
  123. # or <tt>:strict</tt> option set to <tt>true</tt> will always raise
  124. # <tt>ActiveModel::StrictValidationFailed</tt> instead of adding error
  125. # when validation fails. See <tt>validates</tt> for more information about
  126. # the validation itself.
  127. #
  128. # class Person
  129. # include ActiveModel::Validations
  130. #
  131. # attr_accessor :name
  132. # validates! :name, presence: true
  133. # end
  134. #
  135. # person = Person.new
  136. # person.name = ''
  137. # person.valid?
  138. # # => ActiveModel::StrictValidationFailed: Name can't be blank
  139. 1 def validates!(*attributes)
  140. 1 options = attributes.extract_options!
  141. 1 options[:strict] = true
  142. 1 validates(*(attributes << options))
  143. end
  144. 1 private
  145. # When creating custom validators, it might be useful to be able to specify
  146. # additional default keys. This can be done by overwriting this method.
  147. 1 def _validates_default_keys
  148. 37 [:if, :unless, :on, :allow_blank, :allow_nil, :strict]
  149. end
  150. 1 def _parse_validates_options(options)
  151. 40 case options
  152. when TrueClass
  153. 22 {}
  154. when Hash
  155. 11 options
  156. when Range, Array
  157. 2 { in: options }
  158. else
  159. 5 { with: options }
  160. end
  161. end
  162. end
  163. end
  164. end

lib/active_model/validations/with.rb

100.0% lines covered

26 relevant lines. 26 lines covered and 0 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/array/extract_options"
  3. 1 module ActiveModel
  4. 1 module Validations
  5. 1 class WithValidator < EachValidator # :nodoc:
  6. 1 def validate_each(record, attr, val)
  7. 4 method_name = options[:with]
  8. 4 if record.method(method_name).arity == 0
  9. 2 record.send method_name
  10. else
  11. 2 record.send method_name, attr
  12. end
  13. end
  14. end
  15. 1 module ClassMethods
  16. # Passes the record off to the class or classes specified and allows them
  17. # to add errors based on more complex conditions.
  18. #
  19. # class Person
  20. # include ActiveModel::Validations
  21. # validates_with MyValidator
  22. # end
  23. #
  24. # class MyValidator < ActiveModel::Validator
  25. # def validate(record)
  26. # if some_complex_logic
  27. # record.errors.add :base, 'This record is invalid'
  28. # end
  29. # end
  30. #
  31. # private
  32. # def some_complex_logic
  33. # # ...
  34. # end
  35. # end
  36. #
  37. # You may also pass it multiple classes, like so:
  38. #
  39. # class Person
  40. # include ActiveModel::Validations
  41. # validates_with MyValidator, MyOtherValidator, on: :create
  42. # end
  43. #
  44. # Configuration options:
  45. # * <tt>:on</tt> - Specifies the contexts where this validation is active.
  46. # Runs in all validation contexts by default +nil+. You can pass a symbol
  47. # or an array of symbols. (e.g. <tt>on: :create</tt> or
  48. # <tt>on: :custom_validation_context</tt> or
  49. # <tt>on: [:create, :custom_validation_context]</tt>)
  50. # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
  51. # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
  52. # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>).
  53. # The method, proc or string should return or evaluate to a +true+ or
  54. # +false+ value.
  55. # * <tt>:unless</tt> - Specifies a method, proc or string to call to
  56. # determine if the validation should not occur
  57. # (e.g. <tt>unless: :skip_validation</tt>, or
  58. # <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>).
  59. # The method, proc or string should return or evaluate to a +true+ or
  60. # +false+ value.
  61. # * <tt>:strict</tt> - Specifies whether validation should be strict.
  62. # See <tt>ActiveModel::Validations#validates!</tt> for more information.
  63. #
  64. # If you pass any additional configuration options, they will be passed
  65. # to the class and available as +options+:
  66. #
  67. # class Person
  68. # include ActiveModel::Validations
  69. # validates_with MyValidator, my_custom_key: 'my custom value'
  70. # end
  71. #
  72. # class MyValidator < ActiveModel::Validator
  73. # def validate(record)
  74. # options[:my_custom_key] # => "my custom value"
  75. # end
  76. # end
  77. 1 def validates_with(*args, &block)
  78. 377 options = args.extract_options!
  79. 377 options[:class] = self
  80. 377 args.each do |klass|
  81. 378 validator = klass.new(options, &block)
  82. 357 if validator.respond_to?(:attributes) && !validator.attributes.empty?
  83. 345 validator.attributes.each do |attribute|
  84. 369 _validators[attribute.to_sym] << validator
  85. end
  86. else
  87. 12 _validators[nil] << validator
  88. end
  89. 357 validate(validator, options)
  90. end
  91. end
  92. end
  93. # Passes the record off to the class or classes specified and allows them
  94. # to add errors based on more complex conditions.
  95. #
  96. # class Person
  97. # include ActiveModel::Validations
  98. #
  99. # validate :instance_validations
  100. #
  101. # def instance_validations
  102. # validates_with MyValidator
  103. # end
  104. # end
  105. #
  106. # Please consult the class method documentation for more information on
  107. # creating your own validator.
  108. #
  109. # You may also pass it multiple classes, like so:
  110. #
  111. # class Person
  112. # include ActiveModel::Validations
  113. #
  114. # validate :instance_validations, on: :create
  115. #
  116. # def instance_validations
  117. # validates_with MyValidator, MyOtherValidator
  118. # end
  119. # end
  120. #
  121. # Standard configuration options (<tt>:on</tt>, <tt>:if</tt> and
  122. # <tt>:unless</tt>), which are available on the class version of
  123. # +validates_with+, should instead be placed on the +validates+ method
  124. # as these are applied and tested in the callback.
  125. #
  126. # If you pass any additional configuration options, they will be passed
  127. # to the class and available as +options+, please refer to the
  128. # class version of this method for more information.
  129. 1 def validates_with(*args, &block)
  130. 2 options = args.extract_options!
  131. 2 options[:class] = self.class
  132. 2 args.each do |klass|
  133. 2 validator = klass.new(options, &block)
  134. 2 validator.validate(self)
  135. end
  136. end
  137. end
  138. end

lib/active_model/validator.rb

94.59% lines covered

37 relevant lines. 35 lines covered and 2 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require "active_support/core_ext/module/anonymous"
  3. 1 module ActiveModel
  4. # == Active \Model \Validator
  5. #
  6. # A simple base class that can be used along with
  7. # ActiveModel::Validations::ClassMethods.validates_with
  8. #
  9. # class Person
  10. # include ActiveModel::Validations
  11. # validates_with MyValidator
  12. # end
  13. #
  14. # class MyValidator < ActiveModel::Validator
  15. # def validate(record)
  16. # if some_complex_logic
  17. # record.errors.add(:base, "This record is invalid")
  18. # end
  19. # end
  20. #
  21. # private
  22. # def some_complex_logic
  23. # # ...
  24. # end
  25. # end
  26. #
  27. # Any class that inherits from ActiveModel::Validator must implement a method
  28. # called +validate+ which accepts a +record+.
  29. #
  30. # class Person
  31. # include ActiveModel::Validations
  32. # validates_with MyValidator
  33. # end
  34. #
  35. # class MyValidator < ActiveModel::Validator
  36. # def validate(record)
  37. # record # => The person instance being validated
  38. # options # => Any non-standard options passed to validates_with
  39. # end
  40. # end
  41. #
  42. # To cause a validation error, you must add to the +record+'s errors directly
  43. # from within the validators message.
  44. #
  45. # class MyValidator < ActiveModel::Validator
  46. # def validate(record)
  47. # record.errors.add :base, "This is some custom error message"
  48. # record.errors.add :first_name, "This is some complex validation"
  49. # # etc...
  50. # end
  51. # end
  52. #
  53. # To add behavior to the initialize method, use the following signature:
  54. #
  55. # class MyValidator < ActiveModel::Validator
  56. # def initialize(options)
  57. # super
  58. # @my_custom_field = options[:field_name] || :first_name
  59. # end
  60. # end
  61. #
  62. # Note that the validator is initialized only once for the whole application
  63. # life cycle, and not on each validation run.
  64. #
  65. # The easiest way to add custom validators for validating individual attributes
  66. # is with the convenient <tt>ActiveModel::EachValidator</tt>.
  67. #
  68. # class TitleValidator < ActiveModel::EachValidator
  69. # def validate_each(record, attribute, value)
  70. # record.errors.add attribute, 'must be Mr., Mrs., or Dr.' unless %w(Mr. Mrs. Dr.).include?(value)
  71. # end
  72. # end
  73. #
  74. # This can now be used in combination with the +validates+ method
  75. # (see <tt>ActiveModel::Validations::ClassMethods.validates</tt> for more on this).
  76. #
  77. # class Person
  78. # include ActiveModel::Validations
  79. # attr_accessor :title
  80. #
  81. # validates :title, presence: true, title: true
  82. # end
  83. #
  84. # It can be useful to access the class that is using that validator when there are prerequisites such
  85. # as an +attr_accessor+ being present. This class is accessible via <tt>options[:class]</tt> in the constructor.
  86. # To set up your validator override the constructor.
  87. #
  88. # class MyValidator < ActiveModel::Validator
  89. # def initialize(options={})
  90. # super
  91. # options[:class].attr_accessor :custom_attribute
  92. # end
  93. # end
  94. 1 class Validator
  95. 1 attr_reader :options
  96. # Returns the kind of the validator.
  97. #
  98. # PresenceValidator.kind # => :presence
  99. # AcceptanceValidator.kind # => :acceptance
  100. 1 def self.kind
  101. 5 @kind ||= name.split("::").last.underscore.chomp("_validator").to_sym unless anonymous?
  102. end
  103. # Accepts options that will be made available through the +options+ reader.
  104. 1 def initialize(options = {})
  105. 376 @options = options.except(:class).freeze
  106. end
  107. # Returns the kind for this validator.
  108. #
  109. # PresenceValidator.new(attributes: [:username]).kind # => :presence
  110. # AcceptanceValidator.new(attributes: [:terms]).kind # => :acceptance
  111. 1 def kind
  112. 5 self.class.kind
  113. end
  114. # Override this method in subclasses with validation logic, adding errors
  115. # to the records +errors+ array where necessary.
  116. 1 def validate(record)
  117. raise NotImplementedError, "Subclasses must implement a validate(record) method."
  118. end
  119. end
  120. # +EachValidator+ is a validator which iterates through the attributes given
  121. # in the options hash invoking the <tt>validate_each</tt> method passing in the
  122. # record, attribute and value.
  123. #
  124. # All \Active \Model validations are built on top of this validator.
  125. 1 class EachValidator < Validator #:nodoc:
  126. 1 attr_reader :attributes
  127. # Returns a new validator instance. All options will be available via the
  128. # +options+ reader, however the <tt>:attributes</tt> option will be removed
  129. # and instead be made available through the +attributes+ reader.
  130. 1 def initialize(options)
  131. 366 @attributes = Array(options.delete(:attributes))
  132. 366 raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
  133. 365 super
  134. 365 check_validity!
  135. end
  136. # Performs validation on the supplied record. By default this will call
  137. # +validate_each+ to determine validity therefore subclasses should
  138. # override +validate_each+ with validation logic.
  139. 1 def validate(record)
  140. 900 attributes.each do |attribute|
  141. 939 value = read_attribute_for_validation(record, attribute)
  142. 939 next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
  143. 902 validate_each(record, attribute, value)
  144. end
  145. end
  146. # Override this method in subclasses with the validation logic, adding
  147. # errors to the records +errors+ array where necessary.
  148. 1 def validate_each(record, attribute, value)
  149. raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method"
  150. end
  151. # Hook method that gets called by the initializer allowing verification
  152. # that the arguments supplied are valid. You could for example raise an
  153. # +ArgumentError+ when invalid options are supplied.
  154. 1 def check_validity!
  155. end
  156. 1 private
  157. 1 def read_attribute_for_validation(record, attr_name)
  158. 937 record.read_attribute_for_validation(attr_name)
  159. end
  160. end
  161. # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
  162. # and call this block for each attribute being validated. +validates_each+ uses this validator.
  163. 1 class BlockValidator < EachValidator #:nodoc:
  164. 1 def initialize(options, &block)
  165. 2 @block = block
  166. 2 super
  167. end
  168. 1 private
  169. 1 def validate_each(record, attribute, value)
  170. 8 @block.call(record, attribute, value)
  171. end
  172. end
  173. end

lib/active_model/version.rb

75.0% lines covered

4 relevant lines. 3 lines covered and 1 lines missed.
    
  1. # frozen_string_literal: true
  2. 1 require_relative "gem_version"
  3. 1 module ActiveModel
  4. # Returns the version of the currently loaded \Active \Model as a <tt>Gem::Version</tt>
  5. 1 def self.version
  6. gem_version
  7. end
  8. end