Skip to content

Commit

Permalink
Add support for single record serialization
Browse files Browse the repository at this point in the history
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`.
  • Loading branch information
felixbuenemann committed Apr 15, 2016
1 parent f302397 commit 52fb8d1
Show file tree
Hide file tree
Showing 6 changed files with 108 additions and 6 deletions.
1 change: 1 addition & 0 deletions lib/postgres_ext/serializers.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
1 change: 1 addition & 0 deletions lib/postgres_ext/serializers/active_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ module ActiveModel
end

require 'postgres_ext/serializers/active_model/array_serializer'
require 'postgres_ext/serializers/active_model/serializer'
13 changes: 9 additions & 4 deletions lib/postgres_ext/serializers/active_model/array_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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 }
Expand Down
38 changes: 38 additions & 0 deletions lib/postgres_ext/serializers/active_model/serializer.rb
Original file line number Diff line number Diff line change
@@ -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
60 changes: 58 additions & 2 deletions test/serializer_test.rb
Original file line number Diff line number Diff line change
@@ -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 }
Expand Down Expand Up @@ -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
1 change: 1 addition & 0 deletions test/sideloading_test.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
require 'test_helper'

describe 'ArraySerializer patch' do
let(:json_data) { ActiveModel::Serializer.build_json(controller, relation, options).to_json }
Expand Down

0 comments on commit 52fb8d1

Please sign in to comment.