From 52fb8d1386257b4cabbf9cf97a7311cdc92dc664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Felix=20Bu=CC=88nemann?= Date: Fri, 15 Apr 2016 23:52:06 +0200 Subject: [PATCH] Add support for single record serialization This will handle serialization of single records as used for example in show actions using the postres array serializer. To be able to do this, the record is wrapped in a relation using its primary key for lookup. It's also possible to use this feature with a relation by passing the `root: 'singular_name'` and `single_record: true` options to render or the serializer, but you have to ensure that the relation only returns a single record, for example by using `.limit(1)`. The single record serialization format does use a singular root key name for the main resource and does not wrap the object in an array: Collection: `{"notes":[{"id":1,"tag_ids":[1]}],"tags":[...]}` Single Record: `{"note":{"id":1,"tag_ids":[1]},"tags":[...]}` This adds a patch to ActiveModel::Serializer.build_json which adds the required wrapping in a relation. Note that this method will load and instantiate the record and it's serializer once and then reload it using the primary key in a relation. To avoid this you can skip the find call in the controller and replace it with a relation and pass `:root` and `:single_record` along with the singular relation in the call to `render`. --- lib/postgres_ext/serializers.rb | 1 + lib/postgres_ext/serializers/active_model.rb | 1 + .../active_model/array_serializer.rb | 13 ++-- .../serializers/active_model/serializer.rb | 38 ++++++++++++ test/serializer_test.rb | 60 ++++++++++++++++++- test/sideloading_test.rb | 1 + 6 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 lib/postgres_ext/serializers/active_model/serializer.rb diff --git a/lib/postgres_ext/serializers.rb b/lib/postgres_ext/serializers.rb index 26310d9..46ed575 100644 --- a/lib/postgres_ext/serializers.rb +++ b/lib/postgres_ext/serializers.rb @@ -10,3 +10,4 @@ module Serializers require 'active_model_serializers' ActiveModel::ArraySerializer.send :prepend, PostgresExt::Serializers::ActiveModel::ArraySerializer +ActiveModel::Serializer.send :prepend, PostgresExt::Serializers::ActiveModel::Serializer diff --git a/lib/postgres_ext/serializers/active_model.rb b/lib/postgres_ext/serializers/active_model.rb index 38b5e4a..d8aed25 100644 --- a/lib/postgres_ext/serializers/active_model.rb +++ b/lib/postgres_ext/serializers/active_model.rb @@ -4,3 +4,4 @@ module ActiveModel end require 'postgres_ext/serializers/active_model/array_serializer' +require 'postgres_ext/serializers/active_model/serializer' diff --git a/lib/postgres_ext/serializers/active_model/array_serializer.rb b/lib/postgres_ext/serializers/active_model/array_serializer.rb index 355e9a0..5ec24c0 100644 --- a/lib/postgres_ext/serializers/active_model/array_serializer.rb +++ b/lib/postgres_ext/serializers/active_model/array_serializer.rb @@ -29,7 +29,7 @@ def _postgres_serializable_array _reset_internal_state! _include_relation_in_root(object, serializer: @options[:each_serializer], root: @options[:root]) - jsons_select_manager = _results_table_arel + jsons_select_manager = _results_table_arel(@options[:single_record] ? @options[:root] : nil) jsons_select_manager.with @_ctes @_connection.select_value _to_sql(jsons_select_manager) @@ -202,7 +202,7 @@ def _coalesce_arrays(column, aliaz = nil) _postgres_function_node 'COALESCE', [column, Arel.sql("'{}'")], aliaz end - def _results_table_arel + def _results_table_arel(singular_key = nil) tables = [] @_results_tables.each do |key, array| json_table = array @@ -211,9 +211,14 @@ def _results_table_arel json_table = Arel::Nodes::As.new json_table, Arel.sql("tbl") json_table = Arel::Table.new(:t).from(json_table) - json_select_manager = @_connection.send('postgresql_version') >= 90300 ? - json_table.project("COALESCE(json_agg(tbl), '[]') as #{key}, 1 as match") : + json_select_manager = if singular_key.to_s == key.to_s + # Single resource is embedded as object instead of array. + json_table.project("COALESCE(row_to_json(tbl), '{}') as #{key}, 1 as match") + elsif @_connection.send('postgresql_version') >= 90300 + json_table.project("COALESCE(json_agg(tbl), '[]') as #{key}, 1 as match") + else json_table.project("COALESCE(array_to_json(array_agg(row_to_json(tbl))), '[]') as #{key}, 1 as match") + end @_ctes << _postgres_cte_as("#{key}_as_json_array", _to_sql(json_select_manager)) tables << { table: "#{key}_as_json_array", column: key } diff --git a/lib/postgres_ext/serializers/active_model/serializer.rb b/lib/postgres_ext/serializers/active_model/serializer.rb new file mode 100644 index 0000000..2c13841 --- /dev/null +++ b/lib/postgres_ext/serializers/active_model/serializer.rb @@ -0,0 +1,38 @@ +module PostgresExt::Serializers::ActiveModel + module Serializer + def self.prepended(base) + class << base + prepend ClassMethods + end + end + + module ClassMethods + # Wrap ActiveModel::Serializer.build_json + # to send single records through ArraySerializer + # enabling database serialization. + def build_json(controller, resource, options) + serializer_instance = super + return serializer_instance unless serializer_instance + + default_options = controller.send(:default_serializer_options) || {} + options = default_options.merge(options || {}) + + if ActiveRecord::Base === resource && options[:root] != false && serializer_instance.root_name != false + options[:root] ||= serializer_instance.root_name + options[:each_serializer] = serializer_instance.class + options[:single_record] = options.fetch(:single_record, true) + options.delete(:serializer) # Reset to default ArraySerializer. + + # Wrap Record in a Relation. + klass = resource.class + primary_key = klass.primary_key + resource = klass.where(primary_key => resource.send(primary_key)).limit(1) + + super(controller, resource, options) + else + serializer_instance + end + end + end + end +end diff --git a/test/serializer_test.rb b/test/serializer_test.rb index 01510ea..c12d266 100644 --- a/test/serializer_test.rb +++ b/test/serializer_test.rb @@ -1,8 +1,8 @@ require 'test_helper' - describe 'ArraySerializer patch' do - let(:json_data) { ActiveModel::Serializer.build_json(controller, relation, options).to_json } + let(:serializer) { ActiveModel::Serializer.build_json(controller, relation, options) } + let(:json_data) { serializer.to_json } context 'specify serializer' do let(:relation) { Note.all } @@ -70,4 +70,60 @@ json_data.must_equal json_expected end end + + context 'serialize singular record' do + let(:relation) { Note.where(name: 'Title').first } + let(:controller) { NotesController.new } + let(:options) { } + + before do + @note = Note.create content: 'Test', name: 'Title' + @tag = Tag.create name: 'My tag', note: @note, popular: true + end + + it 'uses the array serializer' do + serializer.must_be_instance_of ActiveModel::ArraySerializer + end + + it 'generates the proper json output' do + json_expected = %{{"note":{"id":#{@note.id},"content":"Test","name":"Title","tag_ids":[#{@tag.id}]},"tags":[{"id":#{@tag.id},"name":"My tag","note_id":#{@note.id}}]}} + json_data.must_equal json_expected + end + end + + context 'serialize single record with custom serializer' do + let(:relation) { Note.where(name: 'Title').first } + let(:controller) { NotesController.new } + let(:options) { { serializer: OtherNoteSerializer } } + + before do + @note = Note.create content: 'Test', name: 'Title' + @tag = Tag.create name: 'My tag', note: @note + end + + it 'uses the array serializer' do + serializer.must_be_instance_of ActiveModel::ArraySerializer + end + + it 'generates the proper json output' do + json_expected = %{{"other_note":{"id":#{@note.id},"name":"Title","tag_ids":[#{@tag.id}]},"tags":[{"id":#{@tag.id},"name":"My tag"}]}} + json_data.must_equal json_expected + end + end + + context 'force single record mode' do + let(:relation) { Note.where(name: 'Title').limit(1) } + let(:controller) { NotesController.new } + let(:options) { { root: 'note', single_record: true } } + + before do + @note = Note.create content: 'Test', name: 'Title' + @tag = Tag.create name: 'My tag', note: @note, popular: true + end + + it 'generates the proper json output' do + json_expected = %{{"note":{"id":#{@note.id},"content":"Test","name":"Title","tag_ids":[#{@tag.id}]},"tags":[{"id":#{@tag.id},"name":"My tag","note_id":#{@note.id}}]}} + json_data.must_equal json_expected + end + end end diff --git a/test/sideloading_test.rb b/test/sideloading_test.rb index 7521cba..f15f3e1 100644 --- a/test/sideloading_test.rb +++ b/test/sideloading_test.rb @@ -1,3 +1,4 @@ +require 'test_helper' describe 'ArraySerializer patch' do let(:json_data) { ActiveModel::Serializer.build_json(controller, relation, options).to_json }