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