diff --git a/README.md b/README.md index 0ef40239..0efc7354 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ Fast JSON API serialized 250 records in 3.01 ms * [Object Serialization](#object-serialization) * [Compound Document](#compound-document) * [Key Transforms](#key-transforms) + * [Pluralize Type](#pluralize-type) * [Collection Serialization](#collection-serialization) * [Caching](#caching) * [Params](#params) @@ -170,6 +171,28 @@ set_key_transform :dash # "some_key" => "some-key" set_key_transform :underscore # "some_key" => "some_key" ``` +### Pluralize Type +By default fast_jsonapi does not pluralize type names. You can turn pluralization on using this syntax: + +```ruby +class AwardSerializer + include FastJsonapi::ObjectSerializer + belongs_to :actor + pluralize_type true # "award" => "awards" +end +``` + +Relationship types are not automatically pluralized, even when their base types have `pluralize_type` set. Pluralization can be enabled in the relationship definition. + +```ruby +class ActorSerializer + include FastJsonapi::ObjectSerializer + has_many :awards, pluralize_type: true # "award" => "awards" +end +``` + +The most common use case for this feature is to easily migrate from serialization engines that pluralize by default, such as AMS. + ### Attributes Attributes are defined in FastJsonapi using the `attributes` method. This method is also aliased as `attribute`, which is useful when defining a single attribute. diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index b8a24183..e1979879 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -118,6 +118,7 @@ def inherited(subclass) subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present? subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present? subclass.transform_method = transform_method + subclass.pluralized_type = pluralized_type subclass.cache_length = cache_length subclass.race_condition_ttl = race_condition_ttl subclass.data_links = data_links.dup if data_links.present? @@ -126,6 +127,10 @@ def inherited(subclass) subclass.meta_to_serialize = meta_to_serialize end + def transformed_record_type + run_key_transform(run_key_pluralization(record_type)) + end + def reflected_record_type return @reflected_record_type if defined?(@reflected_record_type) @@ -153,6 +158,17 @@ def set_key_transform(transform_name) end end + def pluralize_type(pluralize) + self.pluralized_type = pluralize + + # ensure that the record type is correctly transformed + if record_type + set_type(record_type) + elsif reflected_record_type + set_type(reflected_record_type) + end + end + def run_key_transform(input) if self.transform_method.present? input.to_s.send(*@transform_method).to_sym @@ -161,13 +177,21 @@ def run_key_transform(input) end end + def run_key_pluralization(input) + if self.pluralized_type + input.to_s.pluralize.to_sym + else + input.to_sym + end + end + def use_hyphen warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead') set_key_transform :dash end def set_type(type_name) - self.record_type = run_key_transform(type_name) + self.record_type = run_key_transform(run_key_pluralization(type_name)) end def set_id(id_name = nil, &block) @@ -243,16 +267,18 @@ def create_relationship(base_key, relationship_type, options, block) end Relationship.new( key: options[:key] || run_key_transform(base_key), + base_key: run_key_transform(base_key_sym), name: name, id_method_name: compute_id_method_name( options[:id_method_name], "#{base_serialization_key}#{id_postfix}".to_sym, block ), - record_type: options[:record_type] || run_key_transform(base_key_sym), + record_type: options[:record_type], + pluralize_type: options[:pluralize_type], object_method_name: options[:object_method_name] || name, object_block: block, - serializer: compute_serializer_name(options[:serializer] || base_key_sym), + serializer_name: compute_serializer_name(options[:serializer] || base_key_sym), relationship_type: relationship_type, cached: options[:cached], polymorphic: fetch_polymorphic_option(options), @@ -305,7 +331,7 @@ def validate_includes!(includes) relationships_to_serialize = klass.relationships_to_serialize || {} relationship_to_include = relationships_to_serialize[parsed_include] raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include - klass = relationship_to_include.serializer.to_s.constantize unless relationship_to_include.polymorphic.is_a?(Hash) + klass = relationship_to_include.serializer unless relationship_to_include.polymorphic.is_a?(Hash) end end end diff --git a/lib/fast_jsonapi/relationship.rb b/lib/fast_jsonapi/relationship.rb index 03fcdfd5..82f57d1c 100644 --- a/lib/fast_jsonapi/relationship.rb +++ b/lib/fast_jsonapi/relationship.rb @@ -1,15 +1,17 @@ module FastJsonapi class Relationship - attr_reader :key, :name, :id_method_name, :record_type, :object_method_name, :object_block, :serializer, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :lazy_load_data + attr_reader :key, :base_key, :name, :id_method_name, :pluralized_type, :object_method_name, :object_block, :serializer_name, :relationship_type, :cached, :polymorphic, :conditional_proc, :transform_method, :links, :lazy_load_data def initialize( key:, + base_key:, name:, id_method_name:, record_type:, + pluralize_type:, object_method_name:, object_block:, - serializer:, + serializer_name:, relationship_type:, cached: false, polymorphic:, @@ -19,12 +21,14 @@ def initialize( lazy_load_data: false ) @key = key + @base_key = base_key @name = name @id_method_name = id_method_name - @record_type = record_type + @pluralized_type = pluralize_type + @record_type = run_key_pluralization(record_type) @object_method_name = object_method_name @object_block = object_block - @serializer = serializer + @serializer_name = serializer_name @relationship_type = relationship_type @cached = cached @polymorphic = polymorphic @@ -59,6 +63,16 @@ def include_relationship?(record, serialization_params) end end + def serializer + @serializer ||= @serializer_name.to_s.constantize + end + + def record_type + @record_type || serializer.transformed_record_type + rescue NameError + run_key_transform(run_key_pluralization(base_key)) + end + private def ids_hash_from_record_and_relationship(record, params = {}) @@ -78,8 +92,8 @@ def ids_hash_from_record_and_relationship(record, params = {}) def id_hash_from_record(record, record_types) klass_name = record.class.respond_to?(:model_name) ? record.class.model_name.to_s : record.class.name # memoize the record type within the record_types dictionary, then assigning to record_type: - associated_record_type = record_types[record.class] ||= run_key_transform(klass_name.demodulize.underscore) + associated_record_type = record_types[record.class] ||= run_key_transform(run_key_pluralization(klass_name.demodulize.underscore)) id_hash(record.id, associated_record_type) end @@ -118,5 +132,14 @@ def run_key_transform(input) input.to_sym end end + + def run_key_pluralization(input) + return unless input + if self.pluralized_type + input.to_s.pluralize.to_sym + else + input.to_sym + end + end end end diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index d845b28b..a9dca526 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -16,6 +16,7 @@ class << self :cachable_relationships_to_serialize, :uncachable_relationships_to_serialize, :transform_method, + :pluralized_type, :record_type, :record_id, :cache_length, @@ -122,7 +123,7 @@ def get_included_records(record, includes_list, known_included_objects, fieldset next unless relationship_item.include_relationship?(record, params) unless relationship_item.polymorphic.is_a?(Hash) record_type = relationship_item.record_type - serializer = relationship_item.serializer.to_s.constantize + serializer = relationship_item.serializer end relationship_type = relationship_item.relationship_type diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 205f6452..b6cefeb3 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -12,7 +12,7 @@ end after do - serializer.relationships_to_serialize = {} + MovieSerializer.relationships_to_serialize.delete(children.first) end context 'with namespace' do @@ -120,7 +120,7 @@ end after do - MovieSerializer.relationships_to_serialize = {} + MovieSerializer.relationships_to_serialize.delete(parent.first) end context 'with overrides' do @@ -176,7 +176,7 @@ end after do - MovieSerializer.relationships_to_serialize = {} + MovieSerializer.relationships_to_serialize.delete(partner.first) end context 'with overrides' do @@ -486,4 +486,75 @@ def year_since_release_calculator(release_year) end end end + + describe '#pluralize_type' do + subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash } + + before do + MovieSerializer.pluralize_type pluralize + end + + after do + MovieSerializer.pluralize_type nil + MovieSerializer.set_type :movie + end + + context 'when pluralize is true' do + let(:pluralize) { true } + + it 'returns correct hash which type equals pluralized value' do + expect(serializable_hash[:data][:type]).to eq :movies + end + end + + context 'when pluralize is false' do + let(:pluralize) { false } + + it 'returns correct hash which type equals non-pluralized value' do + expect(serializable_hash[:data][:type]).to eq :movie + end + end + end + + describe '#pluralize_type after #set_type' do + subject(:serializable_hash) { MovieSerializer.new(movie, include: [:actors]).serializable_hash } + let(:type_name) { :film } + + before do + MovieSerializer.set_type type_name + MovieSerializer.pluralize_type true + end + + after do + MovieSerializer.pluralize_type nil + MovieSerializer.set_type :movie + end + + context 'when sets singular type name' do + it 'returns correct hash which type equals transformed set_type value' do + expect(serializable_hash[:data][:type]).to eq :films + end + end + + context 'when sets plural type name' do + it 'returns correct hash which type equals transformed set_type value' do + expect(serializable_hash[:data][:type]).to eq :films + end + end + + context 'when pluralizing a relationship type after #set_type' do + before do + ActorSerializer.pluralize_type true + end + + after do + ActorSerializer.pluralize_type nil + end + + it 'returns correct hash which relationship type equals transformed set_type value' do + expect(serializable_hash[:data][:relationships][:actors][:data][0][:type]).to eq(:actors) + expect(serializable_hash[:included][0][:type]).to eq(:actors) + end + end + end end diff --git a/spec/lib/object_serializer_inheritance_spec.rb b/spec/lib/object_serializer_inheritance_spec.rb index beb74872..a26abdad 100644 --- a/spec/lib/object_serializer_inheritance_spec.rb +++ b/spec/lib/object_serializer_inheritance_spec.rb @@ -95,13 +95,23 @@ class EmployeeSerializer < UserSerializer has_one :account end + class LegacyUserSerializer < UserSerializer + pluralize_type true + end + + class LegacyEmployeeSerializer < LegacyUserSerializer + attributes :location + attributes :compensation + + has_one :account + end + it 'sets the correct record type' do expect(EmployeeSerializer.reflected_record_type).to eq :employee expect(EmployeeSerializer.record_type).to eq :employee end context 'when testing inheritance of attributes' do - it 'includes parent attributes' do subclass_attributes = EmployeeSerializer.attributes_to_serialize superclass_attributes = UserSerializer.attributes_to_serialize @@ -157,12 +167,15 @@ class EmployeeSerializer < UserSerializer end end - context 'when test inheritence of other attributes' do - - it 'inherits the tranform method' do + context 'when testing inheritence of other attributes' do + it 'inherits the transform method' do EmployeeSerializer expect(UserSerializer.transform_method).to eq EmployeeSerializer.transform_method end + it 'inherits pluralized_type' do + LegacyEmployeeSerializer + expect(LegacyUserSerializer.pluralized_type).to eq LegacyEmployeeSerializer.pluralized_type + end end end diff --git a/spec/lib/object_serializer_pluralization_spec.rb b/spec/lib/object_serializer_pluralization_spec.rb new file mode 100644 index 00000000..f2c19944 --- /dev/null +++ b/spec/lib/object_serializer_pluralization_spec.rb @@ -0,0 +1,61 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + class Author + attr_accessor :id, :name + end + + class Book + attr_accessor :id, :name, :authors, :references + + def author_ids + authors.map(&:id) + end + end + + class Song + attr_accessor :id, :name, :artist + end + + class BookSerializer + include FastJsonapi::ObjectSerializer + attributes :name + set_key_transform :dash + has_many :authors, pluralize_type: true + has_many :references, polymorphic: true, pluralize_type: true + pluralize_type true + end + + let(:book) do + book = Book.new + book.id = 1 + book.name = 'Monstrous Regiment' + book + end + + let(:author) do + author = Author.new + author.id = 1 + author.name = 'Terry Pratchett' + author + end + + let(:song) do + song = Song.new + song.id = 1 + song.name = 'Sweet Polly Oliver' + song + end + + context 'when serializing id and type of polymorphic relationships' do + it 'should return correct type when transform_method is specified' do + book.authors = [author] + book.references = [song] + book_hash = BookSerializer.new(book).to_hash + record_type = book_hash[:data][:relationships][:authors][:data][0][:type] + expect(record_type).to eq 'authors'.to_sym + record_type = book_hash[:data][:relationships][:references][:data][0][:type] + expect(record_type).to eq 'songs'.to_sym + end + end +end diff --git a/spec/lib/object_serializer_relationship_param_spec.rb b/spec/lib/object_serializer_relationship_param_spec.rb index d0168145..838f0400 100644 --- a/spec/lib/object_serializer_relationship_param_spec.rb +++ b/spec/lib/object_serializer_relationship_param_spec.rb @@ -5,6 +5,8 @@ context "params option" do let(:hash) { serializer.serializable_hash } + let(:serializer) { MovieSerializer.new(movie, params: params) } + let(:params) { {authorized: true} } before(:context) do class MovieSerializer @@ -16,19 +18,18 @@ class MovieSerializer movie.actors.map(&:agency)[0] if params[:authorized] end - belongs_to :secondary_agency do |movie| + belongs_to :secondary_agency, serializer: AgencySerializer do |movie| movie.actors.map(&:agency)[1] end + + belongs_to :tertiary_agency, record_type: :custom_agency_type do |movie| + movie.actors.last.agency + end end end - context "passing params to the serializer" do - let(:params) { {authorized: true} } - let(:options_with_params) { {params: params} } - + describe "passing params to the serializer" do context "with a single record" do - let(:serializer) { MovieSerializer.new(movie, options_with_params) } - it "handles relationships that use params" do ids = hash[:data][:relationships][:agencies][:data].map{|a| a[:id]} ids.map!(&:to_i) @@ -42,8 +43,7 @@ class MovieSerializer context "with a list of records" do let(:movies) { build_movies(3) } - let(:params) { {authorized: true} } - let(:serializer) { MovieSerializer.new(movies, options_with_params) } + let(:serializer) { MovieSerializer.new(movies, params: params) } it "handles relationship params when passing params to a list of resources" do relationships_hashes = hash[:data].map{|a| a[:relationships][:agencies][:data]}.uniq.flatten @@ -59,5 +59,46 @@ class MovieSerializer end end end + + describe '#record_type' do + let(:relationship) { MovieSerializer.relationships_to_serialize[relationship_name] } + + context 'without any options' do + let(:relationship_name) { :primary_agency } + it 'infers record_type from relation name' do + expect(relationship.record_type).to eq :primary_agency + end + end + + context 'with serializer option' do + let(:relationship_name) { :secondary_agency } + it 'uses type of given serializer' do + expect(relationship.record_type).to eq :agency + end + end + + context 'with record_type option' do + let(:relationship_name) { :tertiary_agency } + it 'uses record_type option' do + expect(relationship.record_type).to eq :custom_agency_type + end + end + + context 'with pluralize_type true' do + let(:relationship_name) { :secondary_agency } + + before(:context) do + AgencySerializer.pluralize_type true + end + + after(:context) do + AgencySerializer.pluralize_type nil + end + + it 'uses record_type option' do + expect(relationship.record_type).to eq :agencies + end + end + end end end diff --git a/spec/shared/examples/object_serializer_class_methods_examples.rb b/spec/shared/examples/object_serializer_class_methods_examples.rb index cffce414..f8833cb2 100644 --- a/spec/shared/examples/object_serializer_class_methods_examples.rb +++ b/spec/shared/examples/object_serializer_class_methods_examples.rb @@ -2,7 +2,7 @@ it 'returns correct relationship hash' do expect(relationship).to be_instance_of(FastJsonapi::Relationship) # expect(relationship.keys).to all(be_instance_of(Symbol)) - expect(relationship.serializer).to be serializer + expect(relationship.serializer_name).to be serializer expect(relationship.id_method_name).to be id_method_name expect(relationship.record_type).to be record_type end @@ -12,7 +12,7 @@ it 'returns correctly transformed hash' do expect(hash[:data][0][:attributes]).to have_key(release_year) expect(hash[:data][0][:relationships]).to have_key(movie_type) - expect(hash[:data][0][:relationships][movie_type][:data][:type]).to eq(movie_type) + expect(hash[:data][0][:relationships][movie_type][:data][:type]).to eq(serializer_type) expect(hash[:included][0][:type]).to eq(serializer_type) end end