activemodel 3.2.22.5 → 4.0.0.beta1

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