diff --git a/README.md b/README.md index f4333d1..a03e1a8 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,7 @@ Fast JSON API serialized 250 records in 3.01 ms * [Params](#params) * [Conditional Attributes](#conditional-attributes) * [Conditional Relationships](#conditional-relationships) + * [Sparse Fieldsets](#sparse-fieldsets) * [Contributing](#contributing) @@ -207,6 +208,18 @@ class MovieSerializer end ``` +Attributes can also use a different name by passing the original method or accessor with a proc shortcut: + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attributes :name + + attribute :released_in_year, &:year +end +``` + ### Links Per Object Links are defined in FastJsonapi using the `link` method. By default, link are read directly from the model property of the same name.In this example, `public_url` is expected to be a property of the object being serialized. @@ -376,6 +389,21 @@ serializer = MovieSerializer.new(movie, { params: { admin: current_user.admin? } serializer.serializable_hash ``` +### Sparse Fieldsets + +Attributes and relationships can be selectively returned per record type by using the `fields` option. + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + attributes :name, :year +end + +serializer = MovieSerializer.new(movie, { fields: { movie: [:name] } }) +serializer.serializable_hash +``` + ### Customizable Options Option | Purpose | Example diff --git a/lib/fast_jsonapi/attribute.rb b/lib/fast_jsonapi/attribute.rb index c26bf19..9acc3e0 100644 --- a/lib/fast_jsonapi/attribute.rb +++ b/lib/fast_jsonapi/attribute.rb @@ -11,7 +11,7 @@ module FastJsonapi def serialize(record, serialization_params, output_hash) if include_attribute?(record, serialization_params) output_hash[key] = if method.is_a?(Proc) - method.arity == 1 ? method.call(record) : method.call(record, serialization_params) + method.arity.abs == 1 ? method.call(record) : method.call(record, serialization_params) else record.public_send(method) end @@ -26,4 +26,4 @@ module FastJsonapi end end end -end \ No newline at end of file +end diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 90771b4..7f740c8 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -41,8 +41,8 @@ module FastJsonapi return serializable_hash unless @resource - serializable_hash[:data] = self.class.record_hash(@resource, @params) - serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @params) if @includes.present? + serializable_hash[:data] = self.class.record_hash(@resource, @fieldsets[self.class.record_type.to_sym], @params) + serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? serializable_hash end @@ -51,9 +51,10 @@ module FastJsonapi data = [] included = [] + fieldset = @fieldsets[self.class.record_type.to_sym] @resource.each do |record| - data << self.class.record_hash(record, @params) - included.concat self.class.get_included_records(record, @includes, @known_included_objects, @params) if @includes.present? + data << self.class.record_hash(record, fieldset, @params) + included.concat self.class.get_included_records(record, @includes, @known_included_objects, @fieldsets, @params) if @includes.present? end serializable_hash[:data] = data @@ -70,6 +71,8 @@ module FastJsonapi private def process_options(options) + @fieldsets = deep_symbolize(options[:fields].presence || {}) + return if options.blank? @known_included_objects = {} @@ -85,6 +88,18 @@ module FastJsonapi end end + def deep_symbolize(collection) + if collection.is_a? Hash + Hash[collection.map do |k, v| + [k.to_sym, deep_symbolize(v)] + end] + elsif collection.is_a? Array + collection.map { |i| deep_symbolize(i) } + else + collection.to_sym + end + end + def is_collection?(resource, force_is_collection = nil) return force_is_collection unless force_is_collection.nil? @@ -104,6 +119,7 @@ module FastJsonapi subclass.race_condition_ttl = race_condition_ttl subclass.data_links = data_links subclass.cached = cached + subclass.set_type(subclass.reflected_record_type) if subclass.reflected_record_type end def reflected_record_type diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 54b2d81..8d2950a 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -40,27 +40,30 @@ module FastJsonapi end end - def attributes_hash(record, params = {}) - attributes_to_serialize.each_with_object({}) do |(_k, attribute), hash| + def attributes_hash(record, fieldset = nil, params = {}) + attributes = attributes_to_serialize + attributes = attributes.slice(*fieldset) if fieldset.present? + attributes.each_with_object({}) do |(_k, attribute), hash| attribute.serialize(record, params, hash) end end - def relationships_hash(record, relationships = nil, params = {}) + def relationships_hash(record, relationships = nil, fieldset = nil, params = {}) relationships = relationships_to_serialize if relationships.nil? + relationships = relationships.slice(*fieldset) if fieldset.present? relationships.each_with_object({}) do |(_k, relationship), hash| relationship.serialize(record, params, hash) end end - def record_hash(record, params = {}) + def record_hash(record, fieldset, params = {}) if cached record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do temp_hash = id_hash(id_from_record(record), record_type, true) - temp_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present? + temp_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present? temp_hash[:relationships] = {} - temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, params) if cachable_relationships_to_serialize.present? + temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, fieldset, params) if cachable_relationships_to_serialize.present? temp_hash[:links] = links_hash(record, params) if data_links.present? temp_hash end @@ -68,8 +71,8 @@ module FastJsonapi record_hash else record_hash = id_hash(id_from_record(record), record_type, true) - record_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present? - record_hash[:relationships] = relationships_hash(record, nil, params) if relationships_to_serialize.present? + record_hash[:attributes] = attributes_hash(record, fieldset, params) if attributes_to_serialize.present? + record_hash[:relationships] = relationships_hash(record, nil, fieldset, params) if relationships_to_serialize.present? record_hash[:links] = links_hash(record, params) if data_links.present? record_hash end @@ -100,7 +103,7 @@ module FastJsonapi end # includes handler - def get_included_records(record, includes_list, known_included_objects, params = {}) + def get_included_records(record, includes_list, known_included_objects, fieldsets, params = {}) return unless includes_list.present? includes_list.sort.each_with_object([]) do |include_item, included_records| @@ -120,7 +123,7 @@ module FastJsonapi included_objects.each do |inc_obj| if remaining_items(items) - serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, params) + serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects, fieldsets, params) included_records.concat(serializer_records) unless serializer_records.empty? end @@ -128,7 +131,8 @@ module FastJsonapi next if known_included_objects.key?(code) known_included_objects[code] = inc_obj - included_records << serializer.record_hash(inc_obj, params) + + included_records << serializer.record_hash(inc_obj, fieldsets[serializer.record_type], params) end end end diff --git a/lib/fast_jsonapi/version.rb b/lib/fast_jsonapi/version.rb index 3c96404..f17716e 100644 --- a/lib/fast_jsonapi/version.rb +++ b/lib/fast_jsonapi/version.rb @@ -1,3 +1,3 @@ module FastJsonapi - VERSION = "1.2" + VERSION = "1.3" end diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 85346f0..3c477c3 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -230,6 +230,23 @@ describe FastJsonapi::ObjectSerializer do expect(serializable_hash[:data][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})" end end + + context 'with &:proc' do + before do + movie.release_year = 2008 + MovieSerializer.attribute :released_in_year, &:release_year + MovieSerializer.attribute :name, &:local_name + end + + after do + MovieSerializer.attributes_to_serialize.delete(:released_in_year) + end + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:attributes][:name]).to eq "english #{movie.name}" + expect(serializable_hash[:data][:attributes][:released_in_year]).to eq movie.release_year + end + end end describe '#link' do diff --git a/spec/lib/object_serializer_fields_spec.rb b/spec/lib/object_serializer_fields_spec.rb new file mode 100644 index 0000000..913ba83 --- /dev/null +++ b/spec/lib/object_serializer_fields_spec.rb @@ -0,0 +1,48 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + let(:fields) do + { + movie: %i[name actors advertising_campaign], + actor: %i[name agency] + } + end + + it 'only returns specified fields' do + hash = MovieSerializer.new(movie, fields: fields).serializable_hash + + expect(hash[:data][:attributes].keys.sort).to eq %i[name] + end + + it 'only returns specified relationships' do + hash = MovieSerializer.new(movie, fields: fields).serializable_hash + + expect(hash[:data][:relationships].keys.sort).to eq %i[actors advertising_campaign] + end + + it 'only returns specified fields for included relationships' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors]).serializable_hash + + expect(hash[:included].first[:attributes].keys.sort).to eq %i[name] + end + + it 'only returns specified relationships for included relationships' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash + + expect(hash[:included].first[:relationships].keys.sort).to eq %i[agency] + end + + it 'returns all fields for included relationships when no explicit fields have been specified' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash + + expect(hash[:included][3][:attributes].keys.sort).to eq %i[id name] + end + + it 'returns all fields for included relationships when no explicit fields have been specified' do + hash = MovieSerializer.new(movie, fields: fields, include: %i[actors advertising_campaign]).serializable_hash + + expect(hash[:included][3][:relationships].keys.sort).to eq %i[movie] + end +end diff --git a/spec/lib/object_serializer_inheritance_spec.rb b/spec/lib/object_serializer_inheritance_spec.rb index 8cf5b53..beb7487 100644 --- a/spec/lib/object_serializer_inheritance_spec.rb +++ b/spec/lib/object_serializer_inheritance_spec.rb @@ -95,6 +95,11 @@ describe FastJsonapi::ObjectSerializer do 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 diff --git a/spec/lib/serialization_core_spec.rb b/spec/lib/serialization_core_spec.rb index d161e3f..adf33df 100644 --- a/spec/lib/serialization_core_spec.rb +++ b/spec/lib/serialization_core_spec.rb @@ -64,7 +64,7 @@ describe FastJsonapi::ObjectSerializer do known_included_objects = {} included_records = [] [movie, movie].each do |record| - included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects, nil) + included_records.concat MovieSerializer.send(:get_included_records, record, includes_list, known_included_objects, {}, nil) end expect(included_records.size).to eq 3 end diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index dcca065..b92d3d5 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -54,6 +54,10 @@ RSpec.shared_context 'movie class' do "#{id}" end + def local_name(locale = :english) + "#{locale} #{name}" + end + def url "http://movies.com/#{id}" end