activemodel 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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