activemodel 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +172 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model.rb +77 -0
  6. data/lib/active_model/attribute.rb +247 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  8. data/lib/active_model/attribute_assignment.rb +57 -0
  9. data/lib/active_model/attribute_methods.rb +517 -0
  10. data/lib/active_model/attribute_mutation_tracker.rb +178 -0
  11. data/lib/active_model/attribute_set.rb +106 -0
  12. data/lib/active_model/attribute_set/builder.rb +124 -0
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  14. data/lib/active_model/attributes.rb +138 -0
  15. data/lib/active_model/callbacks.rb +156 -0
  16. data/lib/active_model/conversion.rb +111 -0
  17. data/lib/active_model/dirty.rb +280 -0
  18. data/lib/active_model/errors.rb +601 -0
  19. data/lib/active_model/forbidden_attributes_protection.rb +31 -0
  20. data/lib/active_model/gem_version.rb +17 -0
  21. data/lib/active_model/lint.rb +118 -0
  22. data/lib/active_model/locale/en.yml +36 -0
  23. data/lib/active_model/model.rb +99 -0
  24. data/lib/active_model/naming.rb +334 -0
  25. data/lib/active_model/railtie.rb +20 -0
  26. data/lib/active_model/secure_password.rb +128 -0
  27. data/lib/active_model/serialization.rb +192 -0
  28. data/lib/active_model/serializers/json.rb +147 -0
  29. data/lib/active_model/translation.rb +70 -0
  30. data/lib/active_model/type.rb +53 -0
  31. data/lib/active_model/type/big_integer.rb +15 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +47 -0
  34. data/lib/active_model/type/date.rb +53 -0
  35. data/lib/active_model/type/date_time.rb +47 -0
  36. data/lib/active_model/type/decimal.rb +70 -0
  37. data/lib/active_model/type/float.rb +34 -0
  38. data/lib/active_model/type/helpers.rb +7 -0
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +45 -0
  40. data/lib/active_model/type/helpers/mutable.rb +20 -0
  41. data/lib/active_model/type/helpers/numeric.rb +44 -0
  42. data/lib/active_model/type/helpers/time_value.rb +81 -0
  43. data/lib/active_model/type/helpers/timezone.rb +19 -0
  44. data/lib/active_model/type/immutable_string.rb +32 -0
  45. data/lib/active_model/type/integer.rb +58 -0
  46. data/lib/active_model/type/registry.rb +62 -0
  47. data/lib/active_model/type/string.rb +26 -0
  48. data/lib/active_model/type/time.rb +47 -0
  49. data/lib/active_model/type/value.rb +126 -0
  50. data/lib/active_model/validations.rb +437 -0
  51. data/lib/active_model/validations/absence.rb +33 -0
  52. data/lib/active_model/validations/acceptance.rb +102 -0
  53. data/lib/active_model/validations/callbacks.rb +122 -0
  54. data/lib/active_model/validations/clusivity.rb +54 -0
  55. data/lib/active_model/validations/confirmation.rb +80 -0
  56. data/lib/active_model/validations/exclusion.rb +49 -0
  57. data/lib/active_model/validations/format.rb +114 -0
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +47 -0
  60. data/lib/active_model/validations/length.rb +129 -0
  61. data/lib/active_model/validations/numericality.rb +189 -0
  62. data/lib/active_model/validations/presence.rb +39 -0
  63. data/lib/active_model/validations/validates.rb +174 -0
  64. data/lib/active_model/validations/with.rb +147 -0
  65. data/lib/active_model/validator.rb +183 -0
  66. data/lib/active_model/version.rb +10 -0
  67. metadata +125 -0
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model"
4
+ require "rails"
5
+
6
+ module ActiveModel
7
+ class Railtie < Rails::Railtie # :nodoc:
8
+ config.eager_load_namespaces << ActiveModel
9
+
10
+ config.active_model = ActiveSupport::OrderedOptions.new
11
+
12
+ initializer "active_model.secure_password" do
13
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
14
+ end
15
+
16
+ initializer "active_model.i18n_customize_full_message" do
17
+ ActiveModel::Errors.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,128 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module SecurePassword
5
+ extend ActiveSupport::Concern
6
+
7
+ # BCrypt hash function can handle maximum 72 bytes, and if we pass
8
+ # password of length more than 72 bytes it ignores extra characters.
9
+ # Hence need to put a restriction on password length.
10
+ MAX_PASSWORD_LENGTH_ALLOWED = 72
11
+
12
+ class << self
13
+ attr_accessor :min_cost # :nodoc:
14
+ end
15
+ self.min_cost = false
16
+
17
+ module ClassMethods
18
+ # Adds methods to set and authenticate against a BCrypt password.
19
+ # This mechanism requires you to have a +XXX_digest+ attribute.
20
+ # Where +XXX+ is the attribute name of your desired password.
21
+ #
22
+ # The following validations are added automatically:
23
+ # * Password must be present on creation
24
+ # * Password length should be less than or equal to 72 bytes
25
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
26
+ #
27
+ # If confirmation validation is not needed, simply leave out the
28
+ # value for +XXX_confirmation+ (i.e. don't provide a form field for
29
+ # it). When this attribute has a +nil+ value, the validation will not be
30
+ # triggered.
31
+ #
32
+ # For further customizability, it is possible to suppress the default
33
+ # validations by passing <tt>validations: false</tt> as an argument.
34
+ #
35
+ # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
36
+ #
37
+ # gem 'bcrypt', '~> 3.1.7'
38
+ #
39
+ # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
40
+ #
41
+ # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
42
+ # class User < ActiveRecord::Base
43
+ # has_secure_password
44
+ # has_secure_password :recovery_password, validations: false
45
+ # end
46
+ #
47
+ # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
48
+ # user.save # => false, password required
49
+ # user.password = 'mUc3m00RsqyRe'
50
+ # user.save # => false, confirmation doesn't match
51
+ # user.password_confirmation = 'mUc3m00RsqyRe'
52
+ # user.save # => true
53
+ # user.recovery_password = "42password"
54
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
55
+ # user.save # => true
56
+ # user.authenticate('notright') # => false
57
+ # user.authenticate('mUc3m00RsqyRe') # => user
58
+ # user.authenticate_recovery_password('42password') # => user
59
+ # User.find_by(name: 'david').try(:authenticate, 'notright') # => false
60
+ # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
61
+ def has_secure_password(attribute = :password, validations: true)
62
+ # Load bcrypt gem only when has_secure_password is used.
63
+ # This is to avoid ActiveModel (and by extension the entire framework)
64
+ # being dependent on a binary library.
65
+ begin
66
+ require "bcrypt"
67
+ rescue LoadError
68
+ $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
69
+ raise
70
+ end
71
+
72
+ include InstanceMethodsOnActivation.new(attribute)
73
+
74
+ if validations
75
+ include ActiveModel::Validations
76
+
77
+ # This ensures the model has a password by checking whether the password_digest
78
+ # is present, so that this works with both new and existing records. However,
79
+ # when there is an error, the message is added to the password attribute instead
80
+ # so that the error message will make sense to the end-user.
81
+ validate do |record|
82
+ record.errors.add(attribute, :blank) unless record.send("#{attribute}_digest").present?
83
+ end
84
+
85
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
86
+ validates_confirmation_of attribute, allow_blank: true
87
+ end
88
+ end
89
+ end
90
+
91
+ class InstanceMethodsOnActivation < Module
92
+ def initialize(attribute)
93
+ attr_reader attribute
94
+
95
+ define_method("#{attribute}=") do |unencrypted_password|
96
+ if unencrypted_password.nil?
97
+ self.send("#{attribute}_digest=", nil)
98
+ elsif !unencrypted_password.empty?
99
+ instance_variable_set("@#{attribute}", unencrypted_password)
100
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
101
+ self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
102
+ end
103
+ end
104
+
105
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
106
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
107
+ end
108
+
109
+ # Returns +self+ if the password is correct, otherwise +false+.
110
+ #
111
+ # class User < ActiveRecord::Base
112
+ # has_secure_password validations: false
113
+ # end
114
+ #
115
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
116
+ # user.save
117
+ # user.authenticate_password('notright') # => false
118
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
119
+ define_method("authenticate_#{attribute}") do |unencrypted_password|
120
+ attribute_digest = send("#{attribute}_digest")
121
+ BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
122
+ end
123
+
124
+ alias_method :authenticate, :authenticate_password if attribute == :password
125
+ end
126
+ end
127
+ end
128
+ end
@@ -0,0 +1,192 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/hash/except"
4
+ require "active_support/core_ext/hash/slice"
5
+
6
+ module ActiveModel
7
+ # == Active \Model \Serialization
8
+ #
9
+ # Provides a basic serialization to a serializable_hash for your objects.
10
+ #
11
+ # A minimal implementation could be:
12
+ #
13
+ # class Person
14
+ # include ActiveModel::Serialization
15
+ #
16
+ # attr_accessor :name
17
+ #
18
+ # def attributes
19
+ # {'name' => nil}
20
+ # end
21
+ # end
22
+ #
23
+ # Which would provide you with:
24
+ #
25
+ # person = Person.new
26
+ # person.serializable_hash # => {"name"=>nil}
27
+ # person.name = "Bob"
28
+ # person.serializable_hash # => {"name"=>"Bob"}
29
+ #
30
+ # An +attributes+ hash must be defined and should contain any attributes you
31
+ # need to be serialized. Attributes must be strings, not symbols.
32
+ # When called, serializable hash will use instance methods that match the name
33
+ # of the attributes hash's keys. In order to override this behavior, take a look
34
+ # at the private method +read_attribute_for_serialization+.
35
+ #
36
+ # ActiveModel::Serializers::JSON module automatically includes
37
+ # the <tt>ActiveModel::Serialization</tt> module, so there is no need to
38
+ # explicitly include <tt>ActiveModel::Serialization</tt>.
39
+ #
40
+ # A minimal implementation including JSON would be:
41
+ #
42
+ # class Person
43
+ # include ActiveModel::Serializers::JSON
44
+ #
45
+ # attr_accessor :name
46
+ #
47
+ # def attributes
48
+ # {'name' => nil}
49
+ # end
50
+ # end
51
+ #
52
+ # Which would provide you with:
53
+ #
54
+ # person = Person.new
55
+ # person.serializable_hash # => {"name"=>nil}
56
+ # person.as_json # => {"name"=>nil}
57
+ # person.to_json # => "{\"name\":null}"
58
+ #
59
+ # person.name = "Bob"
60
+ # person.serializable_hash # => {"name"=>"Bob"}
61
+ # person.as_json # => {"name"=>"Bob"}
62
+ # person.to_json # => "{\"name\":\"Bob\"}"
63
+ #
64
+ # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
65
+ # <tt>:include</tt>. The following are all valid examples:
66
+ #
67
+ # person.serializable_hash(only: 'name')
68
+ # person.serializable_hash(include: :address)
69
+ # person.serializable_hash(include: { address: { only: 'city' }})
70
+ module Serialization
71
+ # Returns a serialized hash of your object.
72
+ #
73
+ # class Person
74
+ # include ActiveModel::Serialization
75
+ #
76
+ # attr_accessor :name, :age
77
+ #
78
+ # def attributes
79
+ # {'name' => nil, 'age' => nil}
80
+ # end
81
+ #
82
+ # def capitalized_name
83
+ # name.capitalize
84
+ # end
85
+ # end
86
+ #
87
+ # person = Person.new
88
+ # person.name = 'bob'
89
+ # person.age = 22
90
+ # person.serializable_hash # => {"name"=>"bob", "age"=>22}
91
+ # person.serializable_hash(only: :name) # => {"name"=>"bob"}
92
+ # person.serializable_hash(except: :name) # => {"age"=>22}
93
+ # person.serializable_hash(methods: :capitalized_name)
94
+ # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
95
+ #
96
+ # Example with <tt>:include</tt> option
97
+ #
98
+ # class User
99
+ # include ActiveModel::Serializers::JSON
100
+ # attr_accessor :name, :notes # Emulate has_many :notes
101
+ # def attributes
102
+ # {'name' => nil}
103
+ # end
104
+ # end
105
+ #
106
+ # class Note
107
+ # include ActiveModel::Serializers::JSON
108
+ # attr_accessor :title, :text
109
+ # def attributes
110
+ # {'title' => nil, 'text' => nil}
111
+ # end
112
+ # end
113
+ #
114
+ # note = Note.new
115
+ # note.title = 'Battle of Austerlitz'
116
+ # note.text = 'Some text here'
117
+ #
118
+ # user = User.new
119
+ # user.name = 'Napoleon'
120
+ # user.notes = [note]
121
+ #
122
+ # user.serializable_hash
123
+ # # => {"name" => "Napoleon"}
124
+ # user.serializable_hash(include: { notes: { only: 'title' }})
125
+ # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
126
+ def serializable_hash(options = nil)
127
+ options ||= {}
128
+
129
+ attribute_names = attributes.keys
130
+ if only = options[:only]
131
+ attribute_names &= Array(only).map(&:to_s)
132
+ elsif except = options[:except]
133
+ attribute_names -= Array(except).map(&:to_s)
134
+ end
135
+
136
+ hash = {}
137
+ attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
138
+
139
+ Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
140
+
141
+ serializable_add_includes(options) do |association, records, opts|
142
+ hash[association.to_s] = if records.respond_to?(:to_ary)
143
+ records.to_ary.map { |a| a.serializable_hash(opts) }
144
+ else
145
+ records.serializable_hash(opts)
146
+ end
147
+ end
148
+
149
+ hash
150
+ end
151
+
152
+ private
153
+
154
+ # Hook method defining how an attribute value should be retrieved for
155
+ # serialization. By default this is assumed to be an instance named after
156
+ # the attribute. Override this method in subclasses should you need to
157
+ # retrieve the value for a given attribute differently:
158
+ #
159
+ # class MyClass
160
+ # include ActiveModel::Serialization
161
+ #
162
+ # def initialize(data = {})
163
+ # @data = data
164
+ # end
165
+ #
166
+ # def read_attribute_for_serialization(key)
167
+ # @data[key]
168
+ # end
169
+ # end
170
+ alias :read_attribute_for_serialization :send
171
+
172
+ # Add associations specified via the <tt>:include</tt> option.
173
+ #
174
+ # Expects a block that takes as arguments:
175
+ # +association+ - name of the association
176
+ # +records+ - the association record(s) to be serialized
177
+ # +opts+ - options for the association records
178
+ def serializable_add_includes(options = {}) #:nodoc:
179
+ return unless includes = options[:include]
180
+
181
+ unless includes.is_a?(Hash)
182
+ includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
183
+ end
184
+
185
+ includes.each do |association, opts|
186
+ if records = send(association)
187
+ yield association, records, opts
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/json"
4
+
5
+ module ActiveModel
6
+ module Serializers
7
+ # == Active \Model \JSON \Serializer
8
+ module JSON
9
+ extend ActiveSupport::Concern
10
+ include ActiveModel::Serialization
11
+
12
+ included do
13
+ extend ActiveModel::Naming
14
+
15
+ class_attribute :include_root_in_json, instance_writer: false, default: false
16
+ end
17
+
18
+ # Returns a hash representing the model. Some configuration can be
19
+ # passed through +options+.
20
+ #
21
+ # The option <tt>include_root_in_json</tt> controls the top-level behavior
22
+ # of +as_json+. If +true+, +as_json+ will emit a single root node named
23
+ # after the object's type. The default value for <tt>include_root_in_json</tt>
24
+ # option is +false+.
25
+ #
26
+ # user = User.find(1)
27
+ # user.as_json
28
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
29
+ # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
30
+ #
31
+ # ActiveRecord::Base.include_root_in_json = true
32
+ #
33
+ # user.as_json
34
+ # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
35
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
36
+ #
37
+ # This behavior can also be achieved by setting the <tt>:root</tt> option
38
+ # to +true+ as in:
39
+ #
40
+ # user = User.find(1)
41
+ # user.as_json(root: true)
42
+ # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
43
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
44
+ #
45
+ # Without any +options+, the returned Hash will include all the model's
46
+ # attributes.
47
+ #
48
+ # user = User.find(1)
49
+ # user.as_json
50
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
51
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
52
+ #
53
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
54
+ # the attributes included, and work similar to the +attributes+ method.
55
+ #
56
+ # user.as_json(only: [:id, :name])
57
+ # # => { "id" => 1, "name" => "Konata Izumi" }
58
+ #
59
+ # user.as_json(except: [:id, :created_at, :age])
60
+ # # => { "name" => "Konata Izumi", "awesome" => true }
61
+ #
62
+ # To include the result of some method calls on the model use <tt>:methods</tt>:
63
+ #
64
+ # user.as_json(methods: :permalink)
65
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
66
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
67
+ # # "permalink" => "1-konata-izumi" }
68
+ #
69
+ # To include associations use <tt>:include</tt>:
70
+ #
71
+ # user.as_json(include: :posts)
72
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
73
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
74
+ # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
75
+ # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
76
+ #
77
+ # Second level and higher order associations work as well:
78
+ #
79
+ # user.as_json(include: { posts: {
80
+ # include: { comments: {
81
+ # only: :body } },
82
+ # only: :title } })
83
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
84
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
85
+ # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
86
+ # # "title" => "Welcome to the weblog" },
87
+ # # { "comments" => [ { "body" => "Don't think too hard" } ],
88
+ # # "title" => "So I was thinking" } ] }
89
+ def as_json(options = nil)
90
+ root = if options && options.key?(:root)
91
+ options[:root]
92
+ else
93
+ include_root_in_json
94
+ end
95
+
96
+ hash = serializable_hash(options).as_json
97
+ if root
98
+ root = model_name.element if root == true
99
+ { root => hash }
100
+ else
101
+ hash
102
+ end
103
+ end
104
+
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
+ def from_json(json, include_root = include_root_in_json)
140
+ hash = ActiveSupport::JSON.decode(json)
141
+ hash = hash.values.first if include_root
142
+ self.attributes = hash
143
+ self
144
+ end
145
+ end
146
+ end
147
+ end