activemodel 3.2.22.5 → 4.0.0.beta1

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +85 -64
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +61 -24
  5. data/lib/active_model.rb +21 -11
  6. data/lib/active_model/attribute_methods.rb +150 -125
  7. data/lib/active_model/callbacks.rb +49 -34
  8. data/lib/active_model/conversion.rb +39 -19
  9. data/lib/active_model/deprecated_mass_assignment_security.rb +21 -0
  10. data/lib/active_model/dirty.rb +48 -32
  11. data/lib/active_model/errors.rb +176 -88
  12. data/lib/active_model/forbidden_attributes_protection.rb +27 -0
  13. data/lib/active_model/lint.rb +42 -55
  14. data/lib/active_model/locale/en.yml +3 -1
  15. data/lib/active_model/model.rb +97 -0
  16. data/lib/active_model/naming.rb +191 -51
  17. data/lib/active_model/railtie.rb +11 -1
  18. data/lib/active_model/secure_password.rb +55 -25
  19. data/lib/active_model/serialization.rb +51 -27
  20. data/lib/active_model/serializers/json.rb +83 -46
  21. data/lib/active_model/serializers/xml.rb +46 -12
  22. data/lib/active_model/test_case.rb +0 -12
  23. data/lib/active_model/translation.rb +9 -10
  24. data/lib/active_model/validations.rb +154 -52
  25. data/lib/active_model/validations/absence.rb +31 -0
  26. data/lib/active_model/validations/acceptance.rb +10 -22
  27. data/lib/active_model/validations/callbacks.rb +78 -25
  28. data/lib/active_model/validations/clusivity.rb +41 -0
  29. data/lib/active_model/validations/confirmation.rb +13 -23
  30. data/lib/active_model/validations/exclusion.rb +26 -55
  31. data/lib/active_model/validations/format.rb +44 -34
  32. data/lib/active_model/validations/inclusion.rb +22 -52
  33. data/lib/active_model/validations/length.rb +48 -49
  34. data/lib/active_model/validations/numericality.rb +30 -32
  35. data/lib/active_model/validations/presence.rb +12 -22
  36. data/lib/active_model/validations/validates.rb +68 -36
  37. data/lib/active_model/validations/with.rb +28 -23
  38. data/lib/active_model/validator.rb +22 -22
  39. data/lib/active_model/version.rb +4 -4
  40. metadata +23 -24
  41. data/lib/active_model/mass_assignment_security.rb +0 -237
  42. data/lib/active_model/mass_assignment_security/permission_set.rb +0 -40
  43. data/lib/active_model/mass_assignment_security/sanitizer.rb +0 -59
  44. data/lib/active_model/observer_array.rb +0 -147
  45. data/lib/active_model/observing.rb +0 -252
@@ -1,2 +1,12 @@
1
1
  require "active_model"
2
- require "rails"
2
+ require "rails"
3
+
4
+ module ActiveModel
5
+ class Railtie < Rails::Railtie # :nodoc:
6
+ config.eager_load_namespaces << ActiveModel
7
+
8
+ initializer "active_model.secure_password" do
9
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
10
+ end
11
+ end
12
+ end
@@ -2,15 +2,23 @@ module ActiveModel
2
2
  module SecurePassword
3
3
  extend ActiveSupport::Concern
4
4
 
5
+ class << self; attr_accessor :min_cost; end
6
+ self.min_cost = false
7
+
5
8
  module ClassMethods
6
9
  # Adds methods to set and authenticate against a BCrypt password.
7
10
  # This mechanism requires you to have a password_digest attribute.
8
11
  #
9
- # Validations for presence of password, confirmation of password (using
10
- # a "password_confirmation" attribute) are automatically added.
11
- # You can add more validations by hand if need be.
12
+ # Validations for presence of password on create, confirmation of password
13
+ # (using a +password_confirmation+ attribute) are automatically added. If
14
+ # you wish to turn off validations, pass <tt>validations: false</tt> as an
15
+ # argument. You can add more validations by hand if need be.
16
+ #
17
+ # If you don't need the confirmation validation, just don't set any
18
+ # value to the password_confirmation attribute and the the validation
19
+ # will not be triggered.
12
20
  #
13
- # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use has_secure_password:
21
+ # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password:
14
22
  #
15
23
  # gem 'bcrypt-ruby', '~> 3.0.0'
16
24
  #
@@ -21,31 +29,36 @@ module ActiveModel
21
29
  # has_secure_password
22
30
  # end
23
31
  #
24
- # user = User.new(:name => "david", :password => "", :password_confirmation => "nomatch")
32
+ # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
25
33
  # user.save # => false, password required
26
- # user.password = "mUc3m00RsqyRe"
34
+ # user.password = 'mUc3m00RsqyRe'
27
35
  # user.save # => false, confirmation doesn't match
28
- # user.password_confirmation = "mUc3m00RsqyRe"
36
+ # user.password_confirmation = 'mUc3m00RsqyRe'
29
37
  # user.save # => true
30
- # user.authenticate("notright") # => false
31
- # user.authenticate("mUc3m00RsqyRe") # => user
32
- # User.find_by_name("david").try(:authenticate, "notright") # => nil
33
- # User.find_by_name("david").try(:authenticate, "mUc3m00RsqyRe") # => user
34
- def has_secure_password
38
+ # user.authenticate('notright') # => false
39
+ # user.authenticate('mUc3m00RsqyRe') # => user
40
+ # User.find_by_name('david').try(:authenticate, 'notright') # => false
41
+ # User.find_by_name('david').try(:authenticate, 'mUc3m00RsqyRe') # => user
42
+ def has_secure_password(options = {})
35
43
  # Load bcrypt-ruby only when has_secure_password is used.
36
- # This is to avoid ActiveModel (and by extension the entire framework) being dependent on a binary library.
44
+ # This is to avoid ActiveModel (and by extension the entire framework)
45
+ # being dependent on a binary library.
37
46
  gem 'bcrypt-ruby', '~> 3.0.0'
38
47
  require 'bcrypt'
39
48
 
40
49
  attr_reader :password
41
50
 
42
- validates_confirmation_of :password
43
- validates_presence_of :password_digest
51
+ if options.fetch(:validations, true)
52
+ validates_confirmation_of :password
53
+ validates_presence_of :password, :on => :create
54
+
55
+ before_create { raise "Password digest missing on new record" if password_digest.blank? }
56
+ end
44
57
 
45
58
  include InstanceMethodsOnActivation
46
59
 
47
60
  if respond_to?(:attributes_protected_by_default)
48
- def self.attributes_protected_by_default
61
+ def self.attributes_protected_by_default #:nodoc:
49
62
  super + ['password_digest']
50
63
  end
51
64
  end
@@ -53,20 +66,37 @@ module ActiveModel
53
66
  end
54
67
 
55
68
  module InstanceMethodsOnActivation
56
- # Returns self if the password is correct, otherwise false.
69
+ # Returns +self+ if the password is correct, otherwise +false+.
70
+ #
71
+ # class User < ActiveRecord::Base
72
+ # has_secure_password validations: false
73
+ # end
74
+ #
75
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
76
+ # user.save
77
+ # user.authenticate('notright') # => false
78
+ # user.authenticate('mUc3m00RsqyRe') # => user
57
79
  def authenticate(unencrypted_password)
58
- if BCrypt::Password.new(password_digest) == unencrypted_password
59
- self
60
- else
61
- false
62
- end
80
+ BCrypt::Password.new(password_digest) == unencrypted_password && self
63
81
  end
64
82
 
65
- # Encrypts the password into the password_digest attribute.
83
+ # Encrypts the password into the +password_digest+ attribute, only if the
84
+ # new password is not blank.
85
+ #
86
+ # class User < ActiveRecord::Base
87
+ # has_secure_password validations: false
88
+ # end
89
+ #
90
+ # user = User.new
91
+ # user.password = nil
92
+ # user.password_digest # => nil
93
+ # user.password = 'mUc3m00RsqyRe'
94
+ # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
66
95
  def password=(unencrypted_password)
67
- @password = unencrypted_password
68
96
  unless unencrypted_password.blank?
69
- self.password_digest = BCrypt::Password.create(unencrypted_password)
97
+ @password = unencrypted_password
98
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine::DEFAULT_COST
99
+ self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
70
100
  end
71
101
  end
72
102
  end
@@ -1,25 +1,21 @@
1
1
  require 'active_support/core_ext/hash/except'
2
2
  require 'active_support/core_ext/hash/slice'
3
- require 'active_support/core_ext/array/wrap'
4
-
5
3
 
6
4
  module ActiveModel
7
- # == Active Model Serialization
5
+ # == Active \Model \Serialization
8
6
  #
9
7
  # Provides a basic serialization to a serializable_hash for your object.
10
8
  #
11
9
  # A minimal implementation could be:
12
10
  #
13
11
  # class Person
14
- #
15
12
  # include ActiveModel::Serialization
16
13
  #
17
14
  # attr_accessor :name
18
15
  #
19
16
  # def attributes
20
- # {'name' => name}
17
+ # {'name' => nil}
21
18
  # end
22
- #
23
19
  # end
24
20
  #
25
21
  # Which would provide you with:
@@ -29,27 +25,28 @@ module ActiveModel
29
25
  # person.name = "Bob"
30
26
  # person.serializable_hash # => {"name"=>"Bob"}
31
27
  #
32
- # You need to declare some sort of attributes hash which contains the attributes
33
- # you want to serialize and their current value.
28
+ # You need to declare an attributes hash which contains the attributes you
29
+ # want to serialize. Attributes must be strings, not symbols. When called,
30
+ # serializable hash will use instance methods that match the name of the
31
+ # attributes hash's keys. In order to override this behavior, take a look at
32
+ # the private method +read_attribute_for_serialization+.
34
33
  #
35
34
  # Most of the time though, you will want to include the JSON or XML
36
35
  # serializations. Both of these modules automatically include the
37
- # ActiveModel::Serialization module, so there is no need to explicitly
38
- # include it.
36
+ # <tt>ActiveModel::Serialization</tt> module, so there is no need to
37
+ # explicitly include it.
39
38
  #
40
- # So a minimal implementation including XML and JSON would be:
39
+ # A minimal implementation including XML and JSON would be:
41
40
  #
42
41
  # class Person
43
- #
44
42
  # include ActiveModel::Serializers::JSON
45
43
  # include ActiveModel::Serializers::Xml
46
44
  #
47
45
  # attr_accessor :name
48
46
  #
49
47
  # def attributes
50
- # {'name' => name}
48
+ # {'name' => nil}
51
49
  # end
52
- #
53
50
  # end
54
51
  #
55
52
  # Which would provide you with:
@@ -66,27 +63,55 @@ module ActiveModel
66
63
  # person.to_json # => "{\"name\":\"Bob\"}"
67
64
  # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
68
65
  #
69
- # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> .
66
+ # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
67
+ # <tt>:include</tt>. The following are all valid examples:
68
+ #
69
+ # person.serializable_hash(only: 'name')
70
+ # person.serializable_hash(include: :address)
71
+ # person.serializable_hash(include: { address: { only: 'city' }})
70
72
  module Serialization
73
+ # Returns a serialized hash of your object.
74
+ #
75
+ # class Person
76
+ # include ActiveModel::Serialization
77
+ #
78
+ # attr_accessor :name, :age
79
+ #
80
+ # def attributes
81
+ # {'name' => nil, 'age' => nil}
82
+ # end
83
+ #
84
+ # def capitalized_name
85
+ # name.capitalize
86
+ # end
87
+ # end
88
+ #
89
+ # person = Person.new
90
+ # person.name = 'bob'
91
+ # person.age = 22
92
+ # person.serializable_hash # => {"name"=>"bob", "age"=>22}
93
+ # person.serializable_hash(only: :name) # => {"name"=>"bob"}
94
+ # person.serializable_hash(except: :name) # => {"age"=>22}
95
+ # person.serializable_hash(methods: :capitalized_name)
96
+ # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
71
97
  def serializable_hash(options = nil)
72
98
  options ||= {}
73
99
 
74
- attribute_names = attributes.keys.sort
100
+ attribute_names = attributes.keys
75
101
  if only = options[:only]
76
- attribute_names &= Array.wrap(only).map(&:to_s)
102
+ attribute_names &= Array(only).map(&:to_s)
77
103
  elsif except = options[:except]
78
- attribute_names -= Array.wrap(except).map(&:to_s)
104
+ attribute_names -= Array(except).map(&:to_s)
79
105
  end
80
106
 
81
107
  hash = {}
82
108
  attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
83
109
 
84
- method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
85
- method_names.each { |n| hash[n] = send(n) }
110
+ Array(options[:methods]).each { |m| hash[m.to_s] = send(m) if respond_to?(m) }
86
111
 
87
112
  serializable_add_includes(options) do |association, records, opts|
88
- hash[association] = if records.is_a?(Enumerable)
89
- records.map { |a| a.serializable_hash(opts) }
113
+ hash[association.to_s] = if records.respond_to?(:to_ary)
114
+ records.to_ary.map { |a| a.serializable_hash(opts) }
90
115
  else
91
116
  records.serializable_hash(opts)
92
117
  end
@@ -113,7 +138,6 @@ module ActiveModel
113
138
  # @data[key]
114
139
  # end
115
140
  # end
116
- #
117
141
  alias :read_attribute_for_serialization :send
118
142
 
119
143
  # Add associations specified via the <tt>:include</tt> option.
@@ -123,13 +147,13 @@ module ActiveModel
123
147
  # +records+ - the association record(s) to be serialized
124
148
  # +opts+ - options for the association records
125
149
  def serializable_add_includes(options = {}) #:nodoc:
126
- return unless include = options[:include]
150
+ return unless includes = options[:include]
127
151
 
128
- unless include.is_a?(Hash)
129
- include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
152
+ unless includes.is_a?(Hash)
153
+ includes = Hash[Array(includes).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
130
154
  end
131
155
 
132
- include.each do |association, opts|
156
+ includes.each do |association, opts|
133
157
  if records = send(association)
134
158
  yield association, records, opts
135
159
  end
@@ -1,5 +1,4 @@
1
1
  require 'active_support/json'
2
- require 'active_support/core_ext/class/attribute'
3
2
 
4
3
  module ActiveModel
5
4
  module Serializers
@@ -12,83 +11,87 @@ module ActiveModel
12
11
  extend ActiveModel::Naming
13
12
 
14
13
  class_attribute :include_root_in_json
15
- self.include_root_in_json = true
14
+ self.include_root_in_json = false
16
15
  end
17
16
 
18
17
  # Returns a hash representing the model. Some configuration can be
19
18
  # passed through +options+.
20
19
  #
21
20
  # The option <tt>include_root_in_json</tt> controls the top-level behavior
22
- # of +as_json+. If true (the default) +as_json+ will emit a single root
23
- # node named after the object's type. For example:
21
+ # of +as_json+. If +true+, +as_json+ will emit a single root node named
22
+ # after the object's type. The default value for <tt>include_root_in_json</tt>
23
+ # option is +false+.
24
24
  #
25
25
  # user = User.find(1)
26
26
  # user.as_json
27
- # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16,
28
- # "created_at": "2006/08/01", "awesome": true} }
27
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
28
+ # # "created_at" => "2006/08/01", "awesome" => true}
29
+ #
30
+ # ActiveRecord::Base.include_root_in_json = true
29
31
  #
30
- # ActiveRecord::Base.include_root_in_json = false
31
32
  # user.as_json
32
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
33
- # "created_at": "2006/08/01", "awesome": true}
33
+ # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
34
+ # # "created_at" => "2006/08/01", "awesome" => true } }
34
35
  #
35
- # This behavior can also be achieved by setting the <tt>:root</tt> option to +false+ as in:
36
+ # This behavior can also be achieved by setting the <tt>:root</tt> option
37
+ # to +true+ as in:
36
38
  #
37
39
  # user = User.find(1)
38
- # user.as_json(root: false)
39
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
40
- # "created_at": "2006/08/01", "awesome": true}
41
- #
42
- # The remainder of the examples in this section assume include_root_in_json is set to
43
- # <tt>false</tt>.
40
+ # user.as_json(root: true)
41
+ # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
42
+ # # "created_at" => "2006/08/01", "awesome" => true } }
44
43
  #
45
44
  # Without any +options+, the returned Hash will include all the model's
46
- # attributes. For example:
45
+ # attributes.
47
46
  #
48
47
  # user = User.find(1)
49
48
  # user.as_json
50
- # # => {"id": 1, "name": "Konata Izumi", "age": 16,
51
- # "created_at": "2006/08/01", "awesome": true}
49
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
50
+ # # "created_at" => "2006/08/01", "awesome" => true}
52
51
  #
53
- # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit the attributes
54
- # included, and work similar to the +attributes+ method. For example:
52
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
53
+ # the attributes included, and work similar to the +attributes+ method.
55
54
  #
56
- # user.as_json(:only => [ :id, :name ])
57
- # # => {"id": 1, "name": "Konata Izumi"}
55
+ # user.as_json(only: [:id, :name])
56
+ # # => { "id" => 1, "name" => "Konata Izumi" }
58
57
  #
59
- # user.as_json(:except => [ :id, :created_at, :age ])
60
- # # => {"name": "Konata Izumi", "awesome": true}
58
+ # user.as_json(except: [:id, :created_at, :age])
59
+ # # => { "name" => "Konata Izumi", "awesome" => true }
61
60
  #
62
61
  # To include the result of some method calls on the model use <tt>:methods</tt>:
63
62
  #
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"}
63
+ # user.as_json(methods: :permalink)
64
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
65
+ # # "created_at" => "2006/08/01", "awesome" => true,
66
+ # # "permalink" => "1-konata-izumi" }
68
67
  #
69
68
  # To include associations use <tt>:include</tt>:
70
69
  #
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"}]}
70
+ # user.as_json(include: :posts)
71
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
72
+ # # "created_at" => "2006/08/01", "awesome" => true,
73
+ # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
74
+ # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
76
75
  #
77
76
  # Second level and higher order associations work as well:
78
77
  #
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"}]}
78
+ # user.as_json(include: { posts: {
79
+ # include: { comments: {
80
+ # only: :body } },
81
+ # only: :title } })
82
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
83
+ # # "created_at" => "2006/08/01", "awesome" => true,
84
+ # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
85
+ # # "title" => "Welcome to the weblog" },
86
+ # # { "comments" => [ { "body" => "Don't think too hard" } ],
87
+ # # "title" => "So I was thinking" } ] }
89
88
  def as_json(options = nil)
90
- root = include_root_in_json
91
- root = options[:root] if options.try(:key?, :root)
89
+ root = if options && options.key?(:root)
90
+ options[:root]
91
+ else
92
+ include_root_in_json
93
+ end
94
+
92
95
  if root
93
96
  root = self.class.model_name.element if root == true
94
97
  { root => serializable_hash(options) }
@@ -97,6 +100,40 @@ module ActiveModel
97
100
  end
98
101
  end
99
102
 
103
+ # Sets the model +attributes+ from a JSON string. Returns +self+.
104
+ #
105
+ # class Person
106
+ # include ActiveModel::Serializers::JSON
107
+ #
108
+ # attr_accessor :name, :age, :awesome
109
+ #
110
+ # def attributes=(hash)
111
+ # hash.each do |key, value|
112
+ # instance_variable_set("@#{key}", value)
113
+ # end
114
+ # end
115
+ #
116
+ # def attributes
117
+ # instance_values
118
+ # end
119
+ # end
120
+ #
121
+ # json = { name: 'bob', age: 22, awesome:true }.to_json
122
+ # person = Person.new
123
+ # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
124
+ # person.name # => "bob"
125
+ # person.age # => 22
126
+ # person.awesome # => true
127
+ #
128
+ # The default value for +include_root+ is +false+. You can change it to
129
+ # +true+ if the given JSON string includes a single root node.
130
+ #
131
+ # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json
132
+ # person = Person.new
133
+ # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
134
+ # person.name # => "bob"
135
+ # person.age # => 22
136
+ # person.awesome # => true
100
137
  def from_json(json, include_root=include_root_in_json)
101
138
  hash = ActiveSupport::JSON.decode(json)
102
139
  hash = hash.values.first if include_root