omg-activemodel 8.0.0.alpha1

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 (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