omg-activemodel 8.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +99 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +55 -0
  8. data/lib/active_model/attribute.rb +277 -0
  9. data/lib/active_model/attribute_assignment.rb +78 -0
  10. data/lib/active_model/attribute_methods.rb +592 -0
  11. data/lib/active_model/attribute_mutation_tracker.rb +189 -0
  12. data/lib/active_model/attribute_registration.rb +117 -0
  13. data/lib/active_model/attribute_set/builder.rb +182 -0
  14. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  15. data/lib/active_model/attribute_set.rb +118 -0
  16. data/lib/active_model/attributes.rb +165 -0
  17. data/lib/active_model/callbacks.rb +155 -0
  18. data/lib/active_model/conversion.rb +121 -0
  19. data/lib/active_model/deprecator.rb +7 -0
  20. data/lib/active_model/dirty.rb +416 -0
  21. data/lib/active_model/error.rb +208 -0
  22. data/lib/active_model/errors.rb +547 -0
  23. data/lib/active_model/forbidden_attributes_protection.rb +33 -0
  24. data/lib/active_model/gem_version.rb +17 -0
  25. data/lib/active_model/lint.rb +118 -0
  26. data/lib/active_model/locale/en.yml +38 -0
  27. data/lib/active_model/model.rb +78 -0
  28. data/lib/active_model/naming.rb +359 -0
  29. data/lib/active_model/nested_error.rb +22 -0
  30. data/lib/active_model/railtie.rb +24 -0
  31. data/lib/active_model/secure_password.rb +231 -0
  32. data/lib/active_model/serialization.rb +198 -0
  33. data/lib/active_model/serializers/json.rb +154 -0
  34. data/lib/active_model/translation.rb +78 -0
  35. data/lib/active_model/type/big_integer.rb +36 -0
  36. data/lib/active_model/type/binary.rb +62 -0
  37. data/lib/active_model/type/boolean.rb +48 -0
  38. data/lib/active_model/type/date.rb +78 -0
  39. data/lib/active_model/type/date_time.rb +88 -0
  40. data/lib/active_model/type/decimal.rb +107 -0
  41. data/lib/active_model/type/float.rb +64 -0
  42. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
  43. data/lib/active_model/type/helpers/mutable.rb +24 -0
  44. data/lib/active_model/type/helpers/numeric.rb +61 -0
  45. data/lib/active_model/type/helpers/time_value.rb +127 -0
  46. data/lib/active_model/type/helpers/timezone.rb +23 -0
  47. data/lib/active_model/type/helpers.rb +7 -0
  48. data/lib/active_model/type/immutable_string.rb +71 -0
  49. data/lib/active_model/type/integer.rb +113 -0
  50. data/lib/active_model/type/registry.rb +37 -0
  51. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  52. data/lib/active_model/type/string.rb +43 -0
  53. data/lib/active_model/type/time.rb +87 -0
  54. data/lib/active_model/type/value.rb +157 -0
  55. data/lib/active_model/type.rb +55 -0
  56. data/lib/active_model/validations/absence.rb +33 -0
  57. data/lib/active_model/validations/acceptance.rb +113 -0
  58. data/lib/active_model/validations/callbacks.rb +119 -0
  59. data/lib/active_model/validations/clusivity.rb +54 -0
  60. data/lib/active_model/validations/comparability.rb +18 -0
  61. data/lib/active_model/validations/comparison.rb +90 -0
  62. data/lib/active_model/validations/confirmation.rb +80 -0
  63. data/lib/active_model/validations/exclusion.rb +49 -0
  64. data/lib/active_model/validations/format.rb +112 -0
  65. data/lib/active_model/validations/helper_methods.rb +15 -0
  66. data/lib/active_model/validations/inclusion.rb +47 -0
  67. data/lib/active_model/validations/length.rb +130 -0
  68. data/lib/active_model/validations/numericality.rb +222 -0
  69. data/lib/active_model/validations/presence.rb +39 -0
  70. data/lib/active_model/validations/resolve_value.rb +26 -0
  71. data/lib/active_model/validations/validates.rb +175 -0
  72. data/lib/active_model/validations/with.rb +154 -0
  73. data/lib/active_model/validations.rb +489 -0
  74. data/lib/active_model/validator.rb +190 -0
  75. data/lib/active_model/version.rb +10 -0
  76. data/lib/active_model.rb +84 -0
  77. metadata +139 -0
@@ -0,0 +1,231 @@
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
+ # Additionally, a +XXX_challenge+ attribute is created. When set to a
33
+ # value other than +nil+, it will validate against the currently persisted
34
+ # password. This validation relies on dirty tracking, as provided by
35
+ # ActiveModel::Dirty; if dirty tracking methods are not defined, this
36
+ # validation will fail.
37
+ #
38
+ # All of the above validations can be omitted by passing
39
+ # <tt>validations: false</tt> as an argument. This allows complete
40
+ # customizability of validation behavior.
41
+ #
42
+ # Finally, a password reset token that's valid for 15 minutes after issue
43
+ # is automatically configured when +reset_token+ is set to true (which it is by default)
44
+ # and the object reponds to +generates_token_for+ (which Active Records do).
45
+ #
46
+ # To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
47
+ #
48
+ # gem "bcrypt", "~> 3.1.7"
49
+ #
50
+ # ==== Examples
51
+ #
52
+ # ===== Using Active Record (which automatically includes ActiveModel::SecurePassword)
53
+ #
54
+ # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
55
+ # class User < ActiveRecord::Base
56
+ # has_secure_password
57
+ # has_secure_password :recovery_password, validations: false
58
+ # end
59
+ #
60
+ # user = User.new(name: "david", password: "", password_confirmation: "nomatch")
61
+ #
62
+ # user.save # => false, password required
63
+ # user.password = "vr00m"
64
+ # user.save # => false, confirmation doesn't match
65
+ # user.password_confirmation = "vr00m"
66
+ # user.save # => true
67
+ #
68
+ # user.authenticate("notright") # => false
69
+ # user.authenticate("vr00m") # => user
70
+ # User.find_by(name: "david")&.authenticate("notright") # => false
71
+ # User.find_by(name: "david")&.authenticate("vr00m") # => user
72
+ #
73
+ # user.recovery_password = "42password"
74
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
75
+ # user.save # => true
76
+ #
77
+ # user.authenticate_recovery_password("42password") # => user
78
+ #
79
+ # user.update(password: "pwn3d", password_challenge: "") # => false, challenge doesn't authenticate
80
+ # user.update(password: "nohack4u", password_challenge: "vr00m") # => true
81
+ #
82
+ # user.authenticate("vr00m") # => false, old password
83
+ # user.authenticate("nohack4u") # => user
84
+ #
85
+ # ===== Conditionally requiring a password
86
+ #
87
+ # class Account
88
+ # include ActiveModel::SecurePassword
89
+ #
90
+ # attr_accessor :is_guest, :password_digest
91
+ #
92
+ # has_secure_password
93
+ #
94
+ # def errors
95
+ # super.tap { |errors| errors.delete(:password, :blank) if is_guest }
96
+ # end
97
+ # end
98
+ #
99
+ # account = Account.new
100
+ # account.valid? # => false, password required
101
+ #
102
+ # account.is_guest = true
103
+ # account.valid? # => true
104
+ #
105
+ # ===== Using the password reset token
106
+ #
107
+ # user = User.create!(name: "david", password: "123", password_confirmation: "123")
108
+ # token = user.password_reset_token
109
+ # User.find_by_password_reset_token(token) # returns user
110
+ #
111
+ # # 16 minutes later...
112
+ # User.find_by_password_reset_token(token) # returns nil
113
+ #
114
+ # # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
115
+ # User.find_by_password_reset_token!(token)
116
+ def has_secure_password(attribute = :password, validations: true, reset_token: true)
117
+ # Load bcrypt gem only when has_secure_password is used.
118
+ # This is to avoid ActiveModel (and by extension the entire framework)
119
+ # being dependent on a binary library.
120
+ begin
121
+ require "bcrypt"
122
+ rescue LoadError
123
+ warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install."
124
+ raise
125
+ end
126
+
127
+ include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token)
128
+
129
+ if validations
130
+ include ActiveModel::Validations
131
+
132
+ # This ensures the model has a password by checking whether the password_digest
133
+ # is present, so that this works with both new and existing records. However,
134
+ # when there is an error, the message is added to the password attribute instead
135
+ # so that the error message will make sense to the end-user.
136
+ validate do |record|
137
+ record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
138
+ end
139
+
140
+ validate do |record|
141
+ if challenge = record.public_send(:"#{attribute}_challenge")
142
+ digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was")
143
+
144
+ unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge)
145
+ record.errors.add(:"#{attribute}_challenge")
146
+ end
147
+ end
148
+ end
149
+
150
+ # Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes).
151
+ validate do |record|
152
+ password_value = record.public_send(attribute)
153
+ if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
154
+ record.errors.add(attribute, :password_too_long)
155
+ end
156
+ end
157
+
158
+ validates_confirmation_of attribute, allow_blank: true
159
+ end
160
+
161
+ # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
162
+ if reset_token && respond_to?(:generates_token_for)
163
+ generates_token_for :"#{attribute}_reset", expires_in: 15.minutes do
164
+ public_send(:"#{attribute}_salt")&.last(10)
165
+ end
166
+
167
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
168
+ silence_redefinition_of_method :find_by_#{attribute}_reset_token
169
+ def self.find_by_#{attribute}_reset_token(token)
170
+ find_by_token_for(:#{attribute}_reset, token)
171
+ end
172
+
173
+ silence_redefinition_of_method :find_by_#{attribute}_reset_token!
174
+ def self.find_by_#{attribute}_reset_token!(token)
175
+ find_by_token_for!(:#{attribute}_reset, token)
176
+ end
177
+ RUBY
178
+ end
179
+ end
180
+ end
181
+
182
+ class InstanceMethodsOnActivation < Module
183
+ def initialize(attribute, reset_token:)
184
+ attr_reader attribute
185
+
186
+ define_method("#{attribute}=") do |unencrypted_password|
187
+ if unencrypted_password.nil?
188
+ instance_variable_set("@#{attribute}", nil)
189
+ self.public_send("#{attribute}_digest=", nil)
190
+ elsif !unencrypted_password.empty?
191
+ instance_variable_set("@#{attribute}", unencrypted_password)
192
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
193
+ self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
194
+ end
195
+ end
196
+
197
+ attr_accessor :"#{attribute}_confirmation", :"#{attribute}_challenge"
198
+
199
+ # Returns +self+ if the password is correct, otherwise +false+.
200
+ #
201
+ # class User < ActiveRecord::Base
202
+ # has_secure_password validations: false
203
+ # end
204
+ #
205
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
206
+ # user.save
207
+ # user.authenticate_password('notright') # => false
208
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
209
+ define_method("authenticate_#{attribute}") do |unencrypted_password|
210
+ attribute_digest = public_send("#{attribute}_digest")
211
+ attribute_digest.present? && BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
212
+ end
213
+
214
+ # Returns the salt, a small chunk of random data added to the password before it's hashed.
215
+ define_method("#{attribute}_salt") do
216
+ attribute_digest = public_send("#{attribute}_digest")
217
+ attribute_digest.present? ? BCrypt::Password.new(attribute_digest).salt : nil
218
+ end
219
+
220
+ alias_method :authenticate, :authenticate_password if attribute == :password
221
+
222
+ if reset_token
223
+ # Returns the class-level configured reset token for the password.
224
+ define_method("#{attribute}_reset_token") do
225
+ generate_token_for(:"#{attribute}_reset")
226
+ end
227
+ end
228
+ end
229
+ end
230
+ end
231
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/enumerable"
4
+
5
+ module ActiveModel
6
+ # = Active \Model \Serialization
7
+ #
8
+ # Provides a basic serialization to a serializable_hash for your objects.
9
+ #
10
+ # A minimal implementation could be:
11
+ #
12
+ # class Person
13
+ # include ActiveModel::Serialization
14
+ #
15
+ # attr_accessor :name
16
+ #
17
+ # def attributes
18
+ # {'name' => nil}
19
+ # end
20
+ # end
21
+ #
22
+ # Which would provide you with:
23
+ #
24
+ # person = Person.new
25
+ # person.serializable_hash # => {"name"=>nil}
26
+ # person.name = "Bob"
27
+ # person.serializable_hash # => {"name"=>"Bob"}
28
+ #
29
+ # An +attributes+ hash must be defined and should contain any attributes you
30
+ # need to be serialized. Attributes must be strings, not symbols.
31
+ # When called, serializable hash will use instance methods that match the name
32
+ # of the attributes hash's keys. In order to override this behavior, take a look
33
+ # at the private method +read_attribute_for_serialization+.
34
+ #
35
+ # ActiveModel::Serializers::JSON module automatically includes
36
+ # the +ActiveModel::Serialization+ module, so there is no need to
37
+ # explicitly include +ActiveModel::Serialization+.
38
+ #
39
+ # A minimal implementation including JSON would be:
40
+ #
41
+ # class Person
42
+ # include ActiveModel::Serializers::JSON
43
+ #
44
+ # attr_accessor :name
45
+ #
46
+ # def attributes
47
+ # {'name' => nil}
48
+ # end
49
+ # end
50
+ #
51
+ # Which would provide you with:
52
+ #
53
+ # person = Person.new
54
+ # person.serializable_hash # => {"name"=>nil}
55
+ # person.as_json # => {"name"=>nil}
56
+ # person.to_json # => "{\"name\":null}"
57
+ #
58
+ # person.name = "Bob"
59
+ # person.serializable_hash # => {"name"=>"Bob"}
60
+ # person.as_json # => {"name"=>"Bob"}
61
+ # person.to_json # => "{\"name\":\"Bob\"}"
62
+ #
63
+ # Valid options are <tt>:only</tt>, <tt>:except</tt>, <tt>:methods</tt> and
64
+ # <tt>:include</tt>. The following are all valid examples:
65
+ #
66
+ # person.serializable_hash(only: 'name')
67
+ # person.serializable_hash(include: :address)
68
+ # person.serializable_hash(include: { address: { only: 'city' }})
69
+ module Serialization
70
+ # Returns a serialized hash of your object.
71
+ #
72
+ # class Person
73
+ # include ActiveModel::Serialization
74
+ #
75
+ # attr_accessor :name, :age
76
+ #
77
+ # def attributes
78
+ # {'name' => nil, 'age' => nil}
79
+ # end
80
+ #
81
+ # def capitalized_name
82
+ # name.capitalize
83
+ # end
84
+ # end
85
+ #
86
+ # person = Person.new
87
+ # person.name = 'bob'
88
+ # person.age = 22
89
+ # person.serializable_hash # => {"name"=>"bob", "age"=>22}
90
+ # person.serializable_hash(only: :name) # => {"name"=>"bob"}
91
+ # person.serializable_hash(except: :name) # => {"age"=>22}
92
+ # person.serializable_hash(methods: :capitalized_name)
93
+ # # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
94
+ #
95
+ # Example with <tt>:include</tt> option
96
+ #
97
+ # class User
98
+ # include ActiveModel::Serializers::JSON
99
+ # attr_accessor :name, :notes # Emulate has_many :notes
100
+ # def attributes
101
+ # {'name' => nil}
102
+ # end
103
+ # end
104
+ #
105
+ # class Note
106
+ # include ActiveModel::Serializers::JSON
107
+ # attr_accessor :title, :text
108
+ # def attributes
109
+ # {'title' => nil, 'text' => nil}
110
+ # end
111
+ # end
112
+ #
113
+ # note = Note.new
114
+ # note.title = 'Battle of Austerlitz'
115
+ # note.text = 'Some text here'
116
+ #
117
+ # user = User.new
118
+ # user.name = 'Napoleon'
119
+ # user.notes = [note]
120
+ #
121
+ # user.serializable_hash
122
+ # # => {"name" => "Napoleon"}
123
+ # user.serializable_hash(include: { notes: { only: 'title' }})
124
+ # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
125
+ def serializable_hash(options = nil)
126
+ attribute_names = attribute_names_for_serialization
127
+
128
+ return serializable_attributes(attribute_names) if options.blank?
129
+
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 = serializable_attributes(attribute_names)
137
+
138
+ Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
139
+
140
+ serializable_add_includes(options) do |association, records, opts|
141
+ hash[association.to_s] = if records.respond_to?(:to_ary)
142
+ records.to_ary.map { |a| a.serializable_hash(opts) }
143
+ else
144
+ records.serializable_hash(opts)
145
+ end
146
+ end
147
+
148
+ hash
149
+ end
150
+
151
+ private
152
+ def attribute_names_for_serialization
153
+ attributes.keys
154
+ end
155
+
156
+ # Hook method defining how an attribute value should be retrieved for
157
+ # serialization. By default this is assumed to be an instance named after
158
+ # the attribute. Override this method in subclasses should you need to
159
+ # retrieve the value for a given attribute differently:
160
+ #
161
+ # class MyClass
162
+ # include ActiveModel::Serialization
163
+ #
164
+ # def initialize(data = {})
165
+ # @data = data
166
+ # end
167
+ #
168
+ # def read_attribute_for_serialization(key)
169
+ # @data[key]
170
+ # end
171
+ # end
172
+ alias :read_attribute_for_serialization :send
173
+
174
+ def serializable_attributes(attribute_names)
175
+ attribute_names.index_with { |n| read_attribute_for_serialization(n) }
176
+ end
177
+
178
+ # Add associations specified via the <tt>:include</tt> option.
179
+ #
180
+ # Expects a block that takes as arguments:
181
+ # +association+ - name of the association
182
+ # +records+ - the association record(s) to be serialized
183
+ # +opts+ - options for the association records
184
+ def serializable_add_includes(options = {}) # :nodoc:
185
+ return unless includes = options[:include]
186
+
187
+ unless includes.is_a?(Hash)
188
+ includes = Hash[Array(includes).flat_map { |n| n.is_a?(Hash) ? n.to_a : [[n, {}]] }]
189
+ end
190
+
191
+ includes.each do |association, opts|
192
+ if records = send(association)
193
+ yield association, records, opts
194
+ end
195
+ end
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,154 @@
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
+ # If you prefer, <tt>:root</tt> may also be set to a custom string key instead as in:
46
+ #
47
+ # user = User.find(1)
48
+ # user.as_json(root: "author")
49
+ # # => { "author" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
50
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
51
+ #
52
+ # Without any +options+, the returned Hash will include all the model's
53
+ # attributes.
54
+ #
55
+ # user = User.find(1)
56
+ # user.as_json
57
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
58
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
59
+ #
60
+ # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
61
+ # the attributes included, and work similar to the +attributes+ method.
62
+ #
63
+ # user.as_json(only: [:id, :name])
64
+ # # => { "id" => 1, "name" => "Konata Izumi" }
65
+ #
66
+ # user.as_json(except: [:id, :created_at, :age])
67
+ # # => { "name" => "Konata Izumi", "awesome" => true }
68
+ #
69
+ # To include the result of some method calls on the model use <tt>:methods</tt>:
70
+ #
71
+ # user.as_json(methods: :permalink)
72
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
73
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
74
+ # # "permalink" => "1-konata-izumi" }
75
+ #
76
+ # To include associations use <tt>:include</tt>:
77
+ #
78
+ # user.as_json(include: :posts)
79
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
80
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
81
+ # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
82
+ # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
83
+ #
84
+ # Second level and higher order associations work as well:
85
+ #
86
+ # user.as_json(include: { posts: {
87
+ # include: { comments: {
88
+ # only: :body } },
89
+ # only: :title } })
90
+ # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
91
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
92
+ # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
93
+ # # "title" => "Welcome to the weblog" },
94
+ # # { "comments" => [ { "body" => "Don't think too hard" } ],
95
+ # # "title" => "So I was thinking" } ] }
96
+ def as_json(options = nil)
97
+ root = if options && options.key?(:root)
98
+ options[:root]
99
+ else
100
+ include_root_in_json
101
+ end
102
+
103
+ hash = serializable_hash(options).as_json
104
+ if root
105
+ root = model_name.element if root == true
106
+ { root => hash }
107
+ else
108
+ hash
109
+ end
110
+ end
111
+
112
+ # Sets the model +attributes+ from a JSON string. Returns +self+.
113
+ #
114
+ # class Person
115
+ # include ActiveModel::Serializers::JSON
116
+ #
117
+ # attr_accessor :name, :age, :awesome
118
+ #
119
+ # def attributes=(hash)
120
+ # hash.each do |key, value|
121
+ # send("#{key}=", value)
122
+ # end
123
+ # end
124
+ #
125
+ # def attributes
126
+ # instance_values
127
+ # end
128
+ # end
129
+ #
130
+ # json = { name: 'bob', age: 22, awesome:true }.to_json
131
+ # person = Person.new
132
+ # person.from_json(json) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
133
+ # person.name # => "bob"
134
+ # person.age # => 22
135
+ # person.awesome # => true
136
+ #
137
+ # The default value for +include_root+ is +false+. You can change it to
138
+ # +true+ if the given JSON string includes a single root node.
139
+ #
140
+ # json = { person: { name: 'bob', age: 22, awesome:true } }.to_json
141
+ # person = Person.new
142
+ # person.from_json(json, true) # => #<Person:0x007fec5e7a0088 @age=22, @awesome=true, @name="bob">
143
+ # person.name # => "bob"
144
+ # person.age # => 22
145
+ # person.awesome # => true
146
+ def from_json(json, include_root = include_root_in_json)
147
+ hash = ActiveSupport::JSON.decode(json)
148
+ hash = hash.values.first if include_root
149
+ self.attributes = hash
150
+ self
151
+ end
152
+ end
153
+ end
154
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ # = Active \Model \Translation
5
+ #
6
+ # Provides integration between your object and the \Rails internationalization
7
+ # (i18n) framework.
8
+ #
9
+ # A minimal implementation could be:
10
+ #
11
+ # class TranslatedPerson
12
+ # extend ActiveModel::Translation
13
+ # end
14
+ #
15
+ # TranslatedPerson.human_attribute_name('my_attribute')
16
+ # # => "My attribute"
17
+ #
18
+ # This also provides the required class methods for hooking into the
19
+ # \Rails internationalization API, including being able to define a
20
+ # class-based +i18n_scope+ and +lookup_ancestors+ to find translations in
21
+ # parent classes.
22
+ module Translation
23
+ include ActiveModel::Naming
24
+
25
+ singleton_class.attr_accessor :raise_on_missing_translations
26
+
27
+ # Returns the +i18n_scope+ for the class. Override if you want custom lookup.
28
+ def i18n_scope
29
+ :activemodel
30
+ end
31
+
32
+ # When localizing a string, it goes through the lookup returned by this
33
+ # method, which is used in ActiveModel::Name#human,
34
+ # ActiveModel::Errors#full_messages and
35
+ # ActiveModel::Translation#human_attribute_name.
36
+ def lookup_ancestors
37
+ ancestors.select { |x| x.respond_to?(:model_name) }
38
+ end
39
+
40
+ MISSING_TRANSLATION = -(2**60) # :nodoc:
41
+
42
+ # Transforms attribute names into a more human format, such as "First name"
43
+ # instead of "first_name".
44
+ #
45
+ # Person.human_attribute_name("first_name") # => "First name"
46
+ #
47
+ # Specify +options+ with additional translating options.
48
+ def human_attribute_name(attribute, options = {})
49
+ attribute = attribute.to_s
50
+
51
+ if attribute.include?(".")
52
+ namespace, _, attribute = attribute.rpartition(".")
53
+ namespace.tr!(".", "/")
54
+
55
+ defaults = lookup_ancestors.map do |klass|
56
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
57
+ end
58
+ defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
59
+ else
60
+ defaults = lookup_ancestors.map do |klass|
61
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
62
+ end
63
+ end
64
+
65
+ raise_on_missing = options.fetch(:raise, Translation.raise_on_missing_translations)
66
+
67
+ defaults << :"attributes.#{attribute}"
68
+ defaults << options[:default] if options[:default]
69
+ defaults << MISSING_TRANSLATION unless raise_on_missing
70
+
71
+ translation = I18n.translate(defaults.shift, count: 1, raise: raise_on_missing, **options, default: defaults)
72
+ translation = attribute.humanize if translation == MISSING_TRANSLATION
73
+ translation
74
+ end
75
+ end
76
+
77
+ ActiveSupport.run_load_hooks(:active_model_translation, Translation)
78
+ end