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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +67 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +266 -0
- data/lib/active_model/access.rb +16 -0
- data/lib/active_model/api.rb +99 -0
- data/lib/active_model/attribute/user_provided_default.rb +55 -0
- data/lib/active_model/attribute.rb +277 -0
- data/lib/active_model/attribute_assignment.rb +78 -0
- data/lib/active_model/attribute_methods.rb +592 -0
- data/lib/active_model/attribute_mutation_tracker.rb +189 -0
- data/lib/active_model/attribute_registration.rb +117 -0
- data/lib/active_model/attribute_set/builder.rb +182 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +118 -0
- data/lib/active_model/attributes.rb +165 -0
- data/lib/active_model/callbacks.rb +155 -0
- data/lib/active_model/conversion.rb +121 -0
- data/lib/active_model/deprecator.rb +7 -0
- data/lib/active_model/dirty.rb +416 -0
- data/lib/active_model/error.rb +208 -0
- data/lib/active_model/errors.rb +547 -0
- data/lib/active_model/forbidden_attributes_protection.rb +33 -0
- data/lib/active_model/gem_version.rb +17 -0
- data/lib/active_model/lint.rb +118 -0
- data/lib/active_model/locale/en.yml +38 -0
- data/lib/active_model/model.rb +78 -0
- data/lib/active_model/naming.rb +359 -0
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +24 -0
- data/lib/active_model/secure_password.rb +231 -0
- data/lib/active_model/serialization.rb +198 -0
- data/lib/active_model/serializers/json.rb +154 -0
- data/lib/active_model/translation.rb +78 -0
- data/lib/active_model/type/big_integer.rb +36 -0
- data/lib/active_model/type/binary.rb +62 -0
- data/lib/active_model/type/boolean.rb +48 -0
- data/lib/active_model/type/date.rb +78 -0
- data/lib/active_model/type/date_time.rb +88 -0
- data/lib/active_model/type/decimal.rb +107 -0
- data/lib/active_model/type/float.rb +64 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
- data/lib/active_model/type/helpers/mutable.rb +24 -0
- data/lib/active_model/type/helpers/numeric.rb +61 -0
- data/lib/active_model/type/helpers/time_value.rb +127 -0
- data/lib/active_model/type/helpers/timezone.rb +23 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +71 -0
- data/lib/active_model/type/integer.rb +113 -0
- data/lib/active_model/type/registry.rb +37 -0
- data/lib/active_model/type/serialize_cast_value.rb +47 -0
- data/lib/active_model/type/string.rb +43 -0
- data/lib/active_model/type/time.rb +87 -0
- data/lib/active_model/type/value.rb +157 -0
- data/lib/active_model/type.rb +55 -0
- data/lib/active_model/validations/absence.rb +33 -0
- data/lib/active_model/validations/acceptance.rb +113 -0
- data/lib/active_model/validations/callbacks.rb +119 -0
- data/lib/active_model/validations/clusivity.rb +54 -0
- data/lib/active_model/validations/comparability.rb +18 -0
- data/lib/active_model/validations/comparison.rb +90 -0
- data/lib/active_model/validations/confirmation.rb +80 -0
- data/lib/active_model/validations/exclusion.rb +49 -0
- data/lib/active_model/validations/format.rb +112 -0
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +47 -0
- data/lib/active_model/validations/length.rb +130 -0
- data/lib/active_model/validations/numericality.rb +222 -0
- data/lib/active_model/validations/presence.rb +39 -0
- data/lib/active_model/validations/resolve_value.rb +26 -0
- data/lib/active_model/validations/validates.rb +175 -0
- data/lib/active_model/validations/with.rb +154 -0
- data/lib/active_model/validations.rb +489 -0
- data/lib/active_model/validator.rb +190 -0
- data/lib/active_model/version.rb +10 -0
- data/lib/active_model.rb +84 -0
- 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
|