diff --git a/Gemfile b/Gemfile index c0a1fca09c..473d9b60fd 100644 --- a/Gemfile +++ b/Gemfile @@ -8,6 +8,8 @@ branch = ENV.fetch("BRANCH", "main") gem "activesupport", github: "rails/rails", branch: branch gem "activemodel", github: "rails/rails", branch: branch gem "activejob", github: "rails/rails", branch: branch +gem "activerecord", github: "rails/rails", branch: branch +gem "sqlite3", branch == "7-0-stable" ? "~> 1.4" : nil gem "rubocop" gem "rubocop-minitest" diff --git a/lib/active_resource/coder.rb b/lib/active_resource/coder.rb index 720ad8c66e..8cc8b2f224 100644 --- a/lib/active_resource/coder.rb +++ b/lib/active_resource/coder.rb @@ -9,16 +9,16 @@ module ActiveResource # database. Decodes values read from the database into Active Resource # instances. # - # class User < ActiveRecord::Base - # serialize :person, coder: ActiveResource::Coder.new(Person) - # end - # # class Person < ActiveResource::Base # schema do # attribute :name, :string # end # end # + # class User < ActiveRecord::Base + # serialize :person, coder: Person + # end + # # user = User.new # user.person = Person.new name: "Matz" # user.person.name # => "Matz" @@ -37,8 +37,8 @@ module ActiveResource # # user.person_before_type_cast # => "{\"name\":\"Matz\"}" # - # To customize serialization, pass the method name or a block as the second - # argument: + # To customize serialization, pass the method name or a block that accepts the + # instance as the second argument: # # person = Person.new name: "Matz" # @@ -50,6 +50,10 @@ module ActiveResource class Coder attr_accessor :resource_class, :encoder + # ==== Arguments + # * resource_class Active Resource class that to be coded + # * encoder_method the method to invoke on the instance to encode + # it. Defaults to ActiveResource::Base#encode. def initialize(resource_class, encoder_method = :encode, &block) @resource_class = resource_class @encoder = block || encoder_method @@ -70,6 +74,8 @@ def load(value) return if value.nil? value = resource_class.format.decode(value) if value.is_a?(String) raise ArgumentError.new("expected value to be Hash, but was #{value.class}") unless value.is_a?(Hash) + value = Formats.remove_root(value) if value.keys.first.to_s == resource_class.element_name + resource_class.new(value, value[resource_class.primary_key]) end end diff --git a/lib/active_resource/serialization.rb b/lib/active_resource/serialization.rb index 0f2dd46353..e42746c2c0 100644 --- a/lib/active_resource/serialization.rb +++ b/lib/active_resource/serialization.rb @@ -11,16 +11,16 @@ module ActiveResource # database. Decodes strings read from the database into Active Resource # instances. # - # class User < ActiveRecord::Base - # serialize :person, coder: Person - # end - # # class Person < ActiveResource::Base # schema do # attribute :name, :string # end # end # + # class User < ActiveRecord::Base + # serialize :person, coder: Person + # end + # # user = User.new # user.person = Person.new name: "Matz" # @@ -57,6 +57,10 @@ module ActiveResource # end # end # + # class User < ActiveRecord::Base + # serialize :person, coder: ActiveResource::Coder.new(Person, :serializable_hash) + # end + # # user = User.new # user.person = Person.new name: "Matz" # user.person.name # => "Matz" diff --git a/test/cases/base/serialization_test.rb b/test/cases/base/serialization_test.rb index d60adf1565..4eecf1b3fc 100644 --- a/test/cases/base/serialization_test.rb +++ b/test/cases/base/serialization_test.rb @@ -3,7 +3,95 @@ require "abstract_unit" require "fixtures/person" +require "active_record" + +ENV["DATABASE_URL"] = "sqlite3::memory:" + +ActiveRecord::Base.establish_connection + +ActiveRecord::Schema.define do + create_table :users, force: true do |t| + t.text :person_text + t.json :person_json + + t.check_constraint <<~SQL, name: "person_json_is_object" + JSON_TYPE(person_json) = 'object' + SQL + end + + create_table :teams, force: true do |t| + t.text :people_text + t.text :paginated_people_text + t.json :people_json + t.json :paginated_people_json + + t.check_constraint <<~SQL, name: "people_json_is_object" + JSON_TYPE(people_json) = 'array' + SQL + t.check_constraint <<~SQL, name: "paginated_people_json_is_object" + JSON_TYPE(paginated_people_json) = 'object' + SQL + end +end + +class User < ActiveRecord::Base + if ActiveSupport::VERSION::MAJOR < 8 && ActiveSupport::VERSION::MINOR < 1 + serialize :person_text, Person + serialize :person_json, ActiveResource::Coder.new(Person, :serializable_hash) + else + serialize :person_text, coder: Person + serialize :person_json, coder: ActiveResource::Coder.new(Person, :serializable_hash) + end +end + class SerializationTest < ActiveSupport::TestCase + include ActiveRecord::TestFixtures + + test "dumps to a text column" do + resource = Person.new({ id: 1, name: "Matz" }, true) + + User.create!(person_text: resource) + + user = User.sole + assert_equal resource.to_json, user.person_text_before_type_cast + end + + test "dumps to a json column" do + resource = Person.new({ id: 1, name: "Matz" }, true) + + User.create!(person_json: resource) + + user = User.sole + assert_equal resource.serializable_hash.to_json, user.person_json_before_type_cast + end + + test "loads from a text column" do + resource = Person.new(id: 1, name: "Matz") + + User.connection.execute(<<~SQL) + INSERT INTO users(person_text) + VALUES ('#{resource.encode}') + SQL + + user = User.sole + assert_predicate user.person_text, :persisted? + assert_equal resource, user.person_text + assert_equal resource.attributes, user.person_text.attributes + end + + test "loads from a json column" do + resource = Person.new(id: 1, name: "Matz") + + User.connection.execute(<<~SQL) + INSERT INTO users(person_json) + VALUES ('#{resource.encode}') + SQL + + user = User.sole + assert_predicate user.person_json, :persisted? + assert_equal resource, user.person_json + assert_equal resource.attributes, user.person_json.attributes + end test ".load delegates to the .coder" do resource = Person.new(id: 1, name: "Matz")