activemodel 5.2.3

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 +114 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +264 -0
  5. data/lib/active_model.rb +77 -0
  6. data/lib/active_model/attribute.rb +248 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +52 -0
  8. data/lib/active_model/attribute_assignment.rb +57 -0
  9. data/lib/active_model/attribute_methods.rb +478 -0
  10. data/lib/active_model/attribute_mutation_tracker.rb +124 -0
  11. data/lib/active_model/attribute_set.rb +114 -0
  12. data/lib/active_model/attribute_set/builder.rb +126 -0
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
  14. data/lib/active_model/attributes.rb +111 -0
  15. data/lib/active_model/callbacks.rb +153 -0
  16. data/lib/active_model/conversion.rb +111 -0
  17. data/lib/active_model/dirty.rb +343 -0
  18. data/lib/active_model/errors.rb +517 -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 +318 -0
  25. data/lib/active_model/railtie.rb +14 -0
  26. data/lib/active_model/secure_password.rb +129 -0
  27. data/lib/active_model/serialization.rb +192 -0
  28. data/lib/active_model/serializers/json.rb +146 -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 +38 -0
  34. data/lib/active_model/type/date.rb +57 -0
  35. data/lib/active_model/type/date_time.rb +51 -0
  36. data/lib/active_model/type/decimal.rb +70 -0
  37. data/lib/active_model/type/float.rb +36 -0
  38. data/lib/active_model/type/helpers.rb +7 -0
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +41 -0
  40. data/lib/active_model/type/helpers/mutable.rb +20 -0
  41. data/lib/active_model/type/helpers/numeric.rb +37 -0
  42. data/lib/active_model/type/helpers/time_value.rb +68 -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 +70 -0
  46. data/lib/active_model/type/registry.rb +70 -0
  47. data/lib/active_model/type/string.rb +26 -0
  48. data/lib/active_model/type/time.rb +51 -0
  49. data/lib/active_model/type/value.rb +126 -0
  50. data/lib/active_model/validations.rb +439 -0
  51. data/lib/active_model/validations/absence.rb +33 -0
  52. data/lib/active_model/validations/acceptance.rb +106 -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,14 @@
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
+ initializer "active_model.secure_password" do
11
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,129 @@
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 +password_digest+ attribute.
20
+ #
21
+ # The following validations are added automatically:
22
+ # * Password must be present on creation
23
+ # * Password length should be less than or equal to 72 bytes
24
+ # * Confirmation of password (using a +password_confirmation+ attribute)
25
+ #
26
+ # If password confirmation validation is not needed, simply leave out the
27
+ # value for +password_confirmation+ (i.e. don't provide a form field for
28
+ # it). When this attribute has a +nil+ value, the validation will not be
29
+ # triggered.
30
+ #
31
+ # For further customizability, it is possible to suppress the default
32
+ # validations by passing <tt>validations: false</tt> as an argument.
33
+ #
34
+ # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
35
+ #
36
+ # gem 'bcrypt', '~> 3.1.7'
37
+ #
38
+ # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
39
+ #
40
+ # # Schema: User(name:string, password_digest:string)
41
+ # class User < ActiveRecord::Base
42
+ # has_secure_password
43
+ # end
44
+ #
45
+ # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
46
+ # user.save # => false, password required
47
+ # user.password = 'mUc3m00RsqyRe'
48
+ # user.save # => false, confirmation doesn't match
49
+ # user.password_confirmation = 'mUc3m00RsqyRe'
50
+ # user.save # => true
51
+ # user.authenticate('notright') # => false
52
+ # user.authenticate('mUc3m00RsqyRe') # => user
53
+ # User.find_by(name: 'david').try(:authenticate, 'notright') # => false
54
+ # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
55
+ def has_secure_password(options = {})
56
+ # Load bcrypt gem only when has_secure_password is used.
57
+ # This is to avoid ActiveModel (and by extension the entire framework)
58
+ # being dependent on a binary library.
59
+ begin
60
+ require "bcrypt"
61
+ rescue LoadError
62
+ $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
63
+ raise
64
+ end
65
+
66
+ include InstanceMethodsOnActivation
67
+
68
+ if options.fetch(:validations, true)
69
+ include ActiveModel::Validations
70
+
71
+ # This ensures the model has a password by checking whether the password_digest
72
+ # is present, so that this works with both new and existing records. However,
73
+ # when there is an error, the message is added to the password attribute instead
74
+ # so that the error message will make sense to the end-user.
75
+ validate do |record|
76
+ record.errors.add(:password, :blank) unless record.password_digest.present?
77
+ end
78
+
79
+ validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
80
+ validates_confirmation_of :password, allow_blank: true
81
+ end
82
+ end
83
+ end
84
+
85
+ module InstanceMethodsOnActivation
86
+ # Returns +self+ if the password is correct, otherwise +false+.
87
+ #
88
+ # class User < ActiveRecord::Base
89
+ # has_secure_password validations: false
90
+ # end
91
+ #
92
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
93
+ # user.save
94
+ # user.authenticate('notright') # => false
95
+ # user.authenticate('mUc3m00RsqyRe') # => user
96
+ def authenticate(unencrypted_password)
97
+ BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
98
+ end
99
+
100
+ attr_reader :password
101
+
102
+ # Encrypts the password into the +password_digest+ attribute, only if the
103
+ # new password is not empty.
104
+ #
105
+ # class User < ActiveRecord::Base
106
+ # has_secure_password validations: false
107
+ # end
108
+ #
109
+ # user = User.new
110
+ # user.password = nil
111
+ # user.password_digest # => nil
112
+ # user.password = 'mUc3m00RsqyRe'
113
+ # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
114
+ def password=(unencrypted_password)
115
+ if unencrypted_password.nil?
116
+ self.password_digest = nil
117
+ elsif !unencrypted_password.empty?
118
+ @password = unencrypted_password
119
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
120
+ self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
121
+ end
122
+ end
123
+
124
+ def password_confirmation=(unencrypted_password)
125
+ @password_confirmation = unencrypted_password
126
+ end
127
+ end
128
+ end
129
+ 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,146 @@
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/01", "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/01", "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/01", "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/01", "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/01", "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/01", "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/01", "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
+ if root
97
+ root = model_name.element if root == true
98
+ { root => serializable_hash(options) }
99
+ else
100
+ serializable_hash(options)
101
+ end
102
+ end
103
+
104
+ # Sets the model +attributes+ from a JSON string. Returns +self+.
105
+ #
106
+ # class Person
107
+ # include ActiveModel::Serializers::JSON
108
+ #
109
+ # attr_accessor :name, :age, :awesome
110
+ #
111
+ # def attributes=(hash)
112
+ # hash.each do |key, value|
113
+ # send("#{key}=", value)
114
+ # end
115
+ # end
116
+ #
117
+ # def attributes
118
+ # instance_values
119
+ # end
120
+ # end
121
+ #
122
+ # json = { name: 'bob', age: 22, awesome:true }.to_json
123
+ # person = Person.new
124
+ # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
125
+ # person.name # => "bob"
126
+ # person.age # => 22
127
+ # person.awesome # => true
128
+ #
129
+ # The default value for +include_root+ is +false+. You can change it to
130
+ # +true+ if the given JSON string includes a single root node.
131
+ #
132
+ # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json
133
+ # person = Person.new
134
+ # person.from_json(json, true) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
135
+ # person.name # => "bob"
136
+ # person.age # => 22
137
+ # person.awesome # => true
138
+ def from_json(json, include_root = include_root_in_json)
139
+ hash = ActiveSupport::JSON.decode(json)
140
+ hash = hash.values.first if include_root
141
+ self.attributes = hash
142
+ self
143
+ end
144
+ end
145
+ end
146
+ end