-
Notifications
You must be signed in to change notification settings - Fork 21.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix decorated type with type_for_attribute
on the serialized attribute
#41139
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -392,6 +392,68 @@ def test_nil_is_always_persisted_as_null | |||||||||||||||||||||||
assert_equal [topic], Topic.where(content: nil) | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
class EncryptedType < ActiveRecord::Type::Text | ||||||||||||||||||||||||
include ActiveModel::Type::Helpers::Mutable | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
attr_reader :subtype, :encryptor | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def initialize(subtype: ActiveModel::Type::String.new) | ||||||||||||||||||||||||
super() | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
@subtype = subtype | ||||||||||||||||||||||||
@encryptor = ActiveSupport::MessageEncryptor.new("abcd" * 8) | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def serialize(value) | ||||||||||||||||||||||||
subtype.serialize(value).yield_self do |cleartext| | ||||||||||||||||||||||||
encryptor.encrypt_and_sign(cleartext) unless cleartext.nil? | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def deserialize(ciphertext) | ||||||||||||||||||||||||
encryptor.decrypt_and_verify(ciphertext) | ||||||||||||||||||||||||
.yield_self { |cleartext| subtype.deserialize(cleartext) } unless ciphertext.nil? | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def changed_in_place?(old, new) | ||||||||||||||||||||||||
if old.nil? | ||||||||||||||||||||||||
!new.nil? | ||||||||||||||||||||||||
else | ||||||||||||||||||||||||
deserialize(old) != new | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def test_decorated_type_with_type_for_attribute | ||||||||||||||||||||||||
old_registry = ActiveRecord::Type.registry | ||||||||||||||||||||||||
ActiveRecord::Type.registry = ActiveRecord::Type.registry.dup | ||||||||||||||||||||||||
ActiveRecord::Type.register :encrypted, EncryptedType | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
klass = Class.new(ActiveRecord::Base) do | ||||||||||||||||||||||||
self.table_name = Topic.table_name | ||||||||||||||||||||||||
store :content | ||||||||||||||||||||||||
attribute :content, :encrypted, subtype: type_for_attribute(:content) | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do users have to provide a subtype or can it be inferred automatically? Feels like attribute should automatically cascade the types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you mean that we should provide a way to inject a subtype to the type which has rails/activerecord/test/cases/serialized_attribute_test.rb Lines 395 to 400 in 70259f5
rails/activerecord/test/cases/serialized_attribute_test.rb Lines 446 to 450 in 70259f5
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what you mean by "automatically cascade the types". In 6.1, there is no reliable way. If an attribute isn't decorated (no In main branch, If we provide a way for decorator officially, I feel that it is better to provide the API separately (e.g. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Wups, sorry, I forgot to get back to this! I'd just like to be able to do: store :content
attribute :content, :encrypted without needing to pass the
That also sounds good 👍 |
||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
topic = klass.create!(content: { trial: true }) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
assert_equal({ "trial" => true }, topic.content) | ||||||||||||||||||||||||
ensure | ||||||||||||||||||||||||
ActiveRecord::Type.registry = old_registry | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def test_decorated_type_with_decorator_block | ||||||||||||||||||||||||
klass = Class.new(ActiveRecord::Base) do | ||||||||||||||||||||||||
self.table_name = Topic.table_name | ||||||||||||||||||||||||
store :content | ||||||||||||||||||||||||
attribute(:content) { |subtype| EncryptedType.new(subtype: subtype) } | ||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I've allowed multiple decorations which can be used by such like |
||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
topic = klass.create!(content: { trial: true }) | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
assert_equal({ "trial" => true }, topic.content) | ||||||||||||||||||||||||
end | ||||||||||||||||||||||||
|
||||||||||||||||||||||||
def test_mutation_detection_does_not_double_serialize | ||||||||||||||||||||||||
coder = Object.new | ||||||||||||||||||||||||
def coder.dump(value) | ||||||||||||||||||||||||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we assume that
attributes_to_define_after_schema_loads[name][0]
will now always be either aProc
ornil
? If so, what about:The
else
condition could be written more neatly as[prev_type_proc, block].compact.reduce(&:>>)
when we drop Ruby 2.5 support, or if we add aProc#>>
polyfill.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
cast_type
will always beProc
or a type object, isn'tnil
.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
i.e.
attribute :attr_name, EncryptedType.new
should work as it is.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, right! I forgot that case. So then perhaps:
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
default = prev_default if cast_type.nil? && default == NO_DEFAULT_PROVIDED
in the above will override database default tonil
accidentally, and[prev_type_proc, block].compact.reduce { |f, g| -> subtype { g[f[subtype]] } }
can benil
.Personally I'm not interested creating extra block especially when
cast_type
is a symbol or a type object which are the most case, just to useProc#>>
in a future.(Wrapping a block for a symbol is to avoid
bin/test
testing issue. I'll fix that in a later PR.)There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. It would require changing
load_schema!
fromto something like
That could be addressed by adding a default value of
[nil, NO_DEFAULT_PROVIDED]
to theattributes_to_define_after_schema_loads
Hash.However, it seems like you have plans for this code, so let's do the way that you prefer! 👍