From ef42fb303126bcae579bd12c0985c4e47afe8bec Mon Sep 17 00:00:00 2001 From: Shishir Kakaraddi Date: Sun, 20 May 2018 15:53:02 -0700 Subject: [PATCH] Changes for version 1.2 (#220) params support in blocks, nested includes etc Co-authored-by: Jodi Showers Co-authored-by: Ryan O'Donnell Co-authored-by: Les Fletcher Co-authored-by: Ankit gupta Co-authored-by: Masato Ohba Co-authored-by: Shuhei Kitagawa Co-authored-by: Zino Co-authored-by: Carlos Solares Co-authored-by: Brandon Buck Co-authored-by: Daniel Roux Co-authored-by: Dillon Welch --- README.md | 93 ++++- fast_jsonapi.gemspec | 9 +- lib/extensions/has_one.rb | 8 +- .../skylight/normalizers/base.rb | 7 + .../skylight/normalizers/serializable_hash.rb | 4 +- .../skylight/normalizers/serialized_json.rb | 4 +- lib/fast_jsonapi/multi_to_json.rb | 2 + lib/fast_jsonapi/object_serializer.rb | 156 ++++---- lib/fast_jsonapi/serialization_core.rb | 148 +++++-- lib/fast_jsonapi/version.rb | 3 + .../skylight/normalizers_require_spec.rb | 14 + .../object_serializer_attribute_param_spec.rb | 118 ++++++ spec/lib/object_serializer_caching_spec.rb | 4 + .../object_serializer_class_methods_spec.rb | 361 +++++++++++++++--- .../lib/object_serializer_inheritance_spec.rb | 163 ++++++++ .../object_serializer_key_transform_spec.rb | 80 ---- .../lib/object_serializer_performance_spec.rb | 3 - ...ject_serializer_relationship_param_spec.rb | 63 +++ spec/lib/object_serializer_set_id_spec.rb | 30 -- spec/lib/object_serializer_spec.rb | 150 +++++++- spec/lib/object_serializer_struct_spec.rb | 12 +- ...ct_serializer_with_attribute_block_spec.rb | 13 - spec/lib/serialization_core_spec.rb | 6 +- spec/shared/contexts/ams_context.rb | 75 +++- spec/shared/contexts/group_context.rb | 36 +- spec/shared/contexts/movie_context.rb | 206 +++++++++- ...bject_serializer_class_methods_examples.rb | 18 + spec/spec_helper.rb | 2 + 28 files changed, 1429 insertions(+), 359 deletions(-) create mode 100644 lib/fast_jsonapi/instrumentation/skylight/normalizers/base.rb create mode 100644 lib/fast_jsonapi/version.rb create mode 100644 spec/lib/instrumentation/skylight/normalizers_require_spec.rb create mode 100644 spec/lib/object_serializer_attribute_param_spec.rb create mode 100644 spec/lib/object_serializer_inheritance_spec.rb delete mode 100644 spec/lib/object_serializer_key_transform_spec.rb create mode 100644 spec/lib/object_serializer_relationship_param_spec.rb delete mode 100644 spec/lib/object_serializer_set_id_spec.rb delete mode 100644 spec/lib/object_serializer_with_attribute_block_spec.rb create mode 100644 spec/shared/examples/object_serializer_class_methods_examples.rb diff --git a/README.md b/README.md index 8c43a86..2cbaa51 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ Fast JSON API serialized 250 records in 3.01 ms * [Key Transforms](#key-transforms) * [Collection Serialization](#collection-serialization) * [Caching](#caching) + * [Params](#params) * [Contributing](#contributing) @@ -173,7 +174,7 @@ By default, attributes are read directly from the model property of the same nam ```ruby class MovieSerializer include FastJsonapi::ObjectSerializer - + attribute :name end ``` @@ -183,9 +184,9 @@ Custom attributes that must be serialized but do not exist on the model can be d ```ruby class MovieSerializer include FastJsonapi::ObjectSerializer - + attributes :name, :year - + attribute :name_with_year do |object| "#{object.name} (#{object.year})" end @@ -197,21 +198,51 @@ The block syntax can also be used to override the property on the object: ```ruby class MovieSerializer include FastJsonapi::ObjectSerializer - + attribute :name do |object| "#{object.name} Part 2" end 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. + +You can configure the method to use on the object for example a link with key `self` will get set to the value returned by a method called `url` on the movie object. + +You can also use a block to define a url as shown in `custom_url`. You can access params in these blocks as well as shown in `personalized_url` + +```ruby +class MovieSerializer + include FastJsonapi::ObjectSerializer + + link :public_url + + link :self, :url + + link :custom_url do |object| + "http://movies.com/#{object.name}-(#{object.year})" + end + + link :personalized_url do |object, params| + "http://movies.com/#{object.name}-#{params[:user].reference_code}" + end +end +``` + ### Compound Document -Support for top-level included member through ` options[:include] `. +Support for top-level and nested included associations through ` options[:include] `. ```ruby options = {} options[:meta] = { total: 2 } -options[:include] = [:actors] +options[:links] = { + self: '...', + next: '...', + prev: '...' +} +options[:include] = [:actors, :'actors.agency', :'actors.agency.state'] MovieSerializer.new([movie, movie], options).serialized_json ``` @@ -219,11 +250,17 @@ MovieSerializer.new([movie, movie], options).serialized_json ```ruby options[:meta] = { total: 2 } +options[:links] = { + self: '...', + next: '...', + prev: '...' +} hash = MovieSerializer.new([movie, movie], options).serializable_hash json_string = MovieSerializer.new([movie, movie], options).serialized_json ``` ### Caching +Requires a `cache_key` method be defined on model: ```ruby class MovieSerializer @@ -234,17 +271,56 @@ class MovieSerializer end ``` +### Params + +In some cases, attribute values might require more information than what is +available on the record, for example, access privileges or other information +related to a current authenticated user. The `options[:params]` value covers these +cases by allowing you to pass in a hash of additional parameters necessary for +your use case. + +Leveraging the new params is easy, when you define a custom attribute or relationship with a +block you opt-in to using params by adding it as a block parameter. + +```ruby +class MovieSerializer + class MovieSerializer + include FastJsonapi::ObjectSerializer + + attributes :name, :year + attribute :can_view_early do |movie, params| + # in here, params is a hash containing the `:current_user` key + params[:current_user].is_employee? ? true : false + end + + belongs_to :primary_agent do |movie, params| + # in here, params is a hash containing the `:current_user` key + params[:current_user].is_employee? ? true : false + end +end + +# ... +current_user = User.find(cookies[:current_user_id]) +serializer = MovieSerializer.new(movie, {params: {current_user: current_user}}) +serializer.serializable_hash +``` + +Custom attributes and relationships that only receive the resource are still possible by defining +the block to only receive one argument. + ### Customizable Options Option | Purpose | Example ------------ | ------------- | ------------- set_type | Type name of Object | ```set_type :movie ``` set_id | ID of Object | ```set_id :owner_id ``` -cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours``` +cache_options | Hash to enable caching and set cache length | ```cache_options enabled: true, cache_length: 12.hours, race_condition_ttl: 10.seconds``` id_method_name | Set custom method name to get ID of an object | ```has_many :locations, id_method_name: :place_ids ``` object_method_name | Set custom method name to get related objects | ```has_many :locations, object_method_name: :places ``` record_type | Set custom Object Type for a relationship | ```belongs_to :owner, record_type: :user``` -serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor``` +serializer | Set custom Serializer for a relationship | ```has_many :actors, serializer: :custom_actor``` or ```has_many :actors, serializer: MyApp::Api::V1::ActorSerializer``` +polymorphic | Allows different record types for a polymorphic association | ```has_many :targets, polymorphic: true``` +polymorphic | Sets custom record types for each object class in a polymorphic association | ```has_many :targets, polymorphic: { Person => :person, Group => :group }``` ### Instrumentation @@ -304,4 +380,3 @@ rspec spec --tag performance:true Join the Netflix Studio Engineering team and help us build gems like this! * [Senior Ruby Engineer](https://jobs.netflix.com/jobs/864893) -* [Senior Platform Engineer](https://jobs.netflix.com/jobs/865783) diff --git a/fast_jsonapi.gemspec b/fast_jsonapi.gemspec index d3f0440..8f46eda 100644 --- a/fast_jsonapi.gemspec +++ b/fast_jsonapi.gemspec @@ -1,12 +1,17 @@ +lib = File.expand_path('lib', __dir__) +$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) + +require "fast_jsonapi/version" + Gem::Specification.new do |gem| gem.name = "fast_jsonapi" - gem.version = "1.1.1" + gem.version = FastJsonapi::VERSION + gem.required_ruby_version = '>= 2.0.0' if gem.respond_to? :required_ruby_version= gem.required_rubygems_version = Gem::Requirement.new(">= 0") if gem.respond_to? :required_rubygems_version= gem.metadata = { "allowed_push_host" => "https://rubygems.org" } if gem.respond_to? :metadata= gem.require_paths = ["lib"] gem.authors = ["Shishir Kakaraddi", "Srinivas Raghunathan", "Adam Gross"] - gem.date = "2018-02-01" gem.description = "JSON API(jsonapi.org) serializer that works with rails and can be used to serialize any kind of ruby objects" gem.email = "" gem.extra_rdoc_files = [ diff --git a/lib/extensions/has_one.rb b/lib/extensions/has_one.rb index 5d68f1b..930ca57 100644 --- a/lib/extensions/has_one.rb +++ b/lib/extensions/has_one.rb @@ -1,8 +1,6 @@ # frozen_string_literal: true -begin - require 'active_record' - +if defined?(::ActiveRecord) ::ActiveRecord::Associations::Builder::HasOne.class_eval do # Based on # https://github.com/rails/rails/blob/master/activerecord/lib/active_record/associations/builder/collection_association.rb#L50 @@ -12,11 +10,11 @@ begin name = reflection.name mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1 def #{name}_id + # if an attribute is already defined with this methods name we should just use it + return read_attribute(__method__) if has_attribute?(__method__) association(:#{name}).reader.try(:id) end CODE end end -rescue LoadError - # active_record can't be loaded so we shouldn't try to monkey-patch it. end diff --git a/lib/fast_jsonapi/instrumentation/skylight/normalizers/base.rb b/lib/fast_jsonapi/instrumentation/skylight/normalizers/base.rb new file mode 100644 index 0000000..83c971d --- /dev/null +++ b/lib/fast_jsonapi/instrumentation/skylight/normalizers/base.rb @@ -0,0 +1,7 @@ +require 'skylight' + +SKYLIGHT_NORMALIZER_BASE_CLASS = begin + ::Skylight::Core::Normalizers::Normalizer +rescue NameError + ::Skylight::Normalizers::Normalizer +end diff --git a/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb index bc27ee4..16a3c62 100644 --- a/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb +++ b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb @@ -1,11 +1,11 @@ -require 'skylight' +require 'fast_jsonapi/instrumentation/skylight/normalizers/base' require 'fast_jsonapi/instrumentation/serializable_hash' module FastJsonapi module Instrumentation module Skylight module Normalizers - class SerializableHash < Skylight::Normalizers::Normalizer + class SerializableHash < SKYLIGHT_NORMALIZER_BASE_CLASS register FastJsonapi::ObjectSerializer::SERIALIZABLE_HASH_NOTIFICATION diff --git a/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb index a04f6c0..3945c27 100644 --- a/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb +++ b/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb @@ -1,11 +1,11 @@ -require 'skylight' +require 'fast_jsonapi/instrumentation/skylight/normalizers/base' require 'fast_jsonapi/instrumentation/serializable_hash' module FastJsonapi module Instrumentation module Skylight module Normalizers - class SerializedJson < Skylight::Normalizers::Normalizer + class SerializedJson < SKYLIGHT_NORMALIZER_BASE_CLASS register FastJsonapi::ObjectSerializer::SERIALIZED_JSON_NOTIFICATION diff --git a/lib/fast_jsonapi/multi_to_json.rb b/lib/fast_jsonapi/multi_to_json.rb index 108579e..7917edb 100644 --- a/lib/fast_jsonapi/multi_to_json.rb +++ b/lib/fast_jsonapi/multi_to_json.rb @@ -1,5 +1,7 @@ # frozen_string_literal: true +require 'logger' + # Usage: # class Movie # def to_json(payload) diff --git a/lib/fast_jsonapi/object_serializer.rb b/lib/fast_jsonapi/object_serializer.rb index 2f5ebe4..4dea074 100644 --- a/lib/fast_jsonapi/object_serializer.rb +++ b/lib/fast_jsonapi/object_serializer.rb @@ -10,8 +10,8 @@ module FastJsonapi extend ActiveSupport::Concern include SerializationCore - SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'.freeze - SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'.freeze + SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash' + SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json' included do # Set record_type based on the name of the serializer class @@ -34,11 +34,12 @@ module FastJsonapi def hash_for_one_record serializable_hash = { data: nil } serializable_hash[:meta] = @meta if @meta.present? + serializable_hash[:links] = @links if @links.present? return serializable_hash unless @resource - serializable_hash[:data] = self.class.record_hash(@resource) - serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects) if @includes.present? + 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 end @@ -48,13 +49,14 @@ module FastJsonapi data = [] included = [] @resource.each do |record| - data << self.class.record_hash(record) - included.concat self.class.get_included_records(record, @includes, @known_included_objects) if @includes.present? + data << self.class.record_hash(record, @params) + included.concat self.class.get_included_records(record, @includes, @known_included_objects, @params) if @includes.present? end serializable_hash[:data] = data serializable_hash[:included] = included if @includes.present? serializable_hash[:meta] = @meta if @meta.present? + serializable_hash[:links] = @links if @links.present? serializable_hash end @@ -69,20 +71,13 @@ module FastJsonapi @known_included_objects = {} @meta = options[:meta] + @links = options[:links] + @params = options[:params] || {} + raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash) if options[:include].present? @includes = options[:include].delete_if(&:blank?).map(&:to_sym) - validate_includes!(@includes) - end - end - - def validate_includes!(includes) - return if includes.blank? - - existing_relationships = self.class.relationships_to_serialize.keys.to_set - - unless existing_relationships.superset?(includes.to_set) - raise ArgumentError, "One of keys from #{includes} is not specified as a relationship on the serializer" + self.class.validate_includes!(@includes) end end @@ -91,6 +86,20 @@ module FastJsonapi end class_methods do + + def inherited(subclass) + super(subclass) + subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present? + subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present? + 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.cache_length = cache_length + subclass.race_condition_ttl = race_condition_ttl + subclass.data_links = data_links + subclass.cached = cached + end + def reflected_record_type return @reflected_record_type if defined?(@reflected_record_type) @@ -108,11 +117,11 @@ module FastJsonapi dash: :dasherize, underscore: :underscore } - @transform_method = mapping[transform_name.to_sym] + self.transform_method = mapping[transform_name.to_sym] end def run_key_transform(input) - if @transform_method.present? + if self.transform_method.present? input.to_s.send(*@transform_method).to_sym else input.to_sym @@ -135,6 +144,7 @@ module FastJsonapi def cache_options(cache_options) self.cached = cache_options[:enabled] || false self.cache_length = cache_options[:cache_length] || 5.minutes + self.race_condition_ttl = cache_options[:race_condition_ttl] || 5.seconds end def attributes(*attributes_list, &block) @@ -162,67 +172,54 @@ module FastJsonapi self.relationships_to_serialize[name] = relationship end - def has_many(relationship_name, options = {}) + def has_many(relationship_name, options = {}, &block) name = relationship_name.to_sym - singular_name = relationship_name.to_s.singularize - serializer_key = options[:serializer] || singular_name.to_sym - key = options[:key] || run_key_transform(relationship_name) - record_type = options[:record_type] || run_key_transform(singular_name) - relationship = { - key: key, + hash = create_relationship_hash(relationship_name, :has_many, options, block) + add_relationship(name, hash) + end + + def has_one(relationship_name, options = {}, &block) + name = relationship_name.to_sym + hash = create_relationship_hash(relationship_name, :has_one, options, block) + add_relationship(name, hash) + end + + def belongs_to(relationship_name, options = {}, &block) + name = relationship_name.to_sym + hash = create_relationship_hash(relationship_name, :belongs_to, options, block) + add_relationship(name, hash) + end + + def create_relationship_hash(base_key, relationship_type, options, block) + name = base_key.to_sym + if relationship_type == :has_many + base_serialization_key = base_key.to_s.singularize + base_key_sym = base_serialization_key.to_sym + id_postfix = '_ids' + else + base_serialization_key = base_key + base_key_sym = name + id_postfix = '_id' + end + { + key: options[:key] || run_key_transform(base_key), name: name, - id_method_name: options[:id_method_name] || (singular_name + '_ids').to_sym, - record_type: record_type, + id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym, + record_type: options[:record_type] || run_key_transform(base_key_sym), object_method_name: options[:object_method_name] || name, - serializer: compute_serializer_name(serializer_key), - relationship_type: :has_many, + object_block: block, + serializer: compute_serializer_name(options[:serializer] || base_key_sym), + relationship_type: relationship_type, cached: options[:cached] || false, polymorphic: fetch_polymorphic_option(options) } - add_relationship(name, relationship) - end - - def belongs_to(relationship_name, options = {}) - name = relationship_name.to_sym - serializer_key = options[:serializer] || relationship_name.to_sym - key = options[:key] || run_key_transform(relationship_name) - record_type = options[:record_type] || run_key_transform(relationship_name) - add_relationship(name, { - key: key, - name: name, - id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym, - record_type: record_type, - object_method_name: options[:object_method_name] || name, - serializer: compute_serializer_name(serializer_key), - relationship_type: :belongs_to, - cached: options[:cached] || true, - polymorphic: fetch_polymorphic_option(options) - }) - end - - def has_one(relationship_name, options = {}) - name = relationship_name.to_sym - serializer_key = options[:serializer] || name - key = options[:key] || run_key_transform(relationship_name) - record_type = options[:record_type] || run_key_transform(relationship_name) - add_relationship(name, { - key: key, - name: name, - id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym, - record_type: record_type, - object_method_name: options[:object_method_name] || name, - serializer: compute_serializer_name(serializer_key), - relationship_type: :has_one, - cached: options[:cached] || false, - polymorphic: fetch_polymorphic_option(options) - }) end def compute_serializer_name(serializer_key) + return serializer_key unless serializer_key.is_a? Symbol namespace = self.name.gsub(/()?\w+Serializer$/, '') serializer_name = serializer_key.to_s.classify + 'Serializer' - return (namespace + serializer_name).to_sym if namespace.present? - (serializer_key.to_s.classify + 'Serializer').to_sym + (namespace + serializer_name).to_sym end def fetch_polymorphic_option(options) @@ -231,6 +228,27 @@ module FastJsonapi return option if option.respond_to? :keys {} end + + def link(link_name, link_method_name = nil, &block) + self.data_links = {} if self.data_links.nil? + link_method_name = link_name if link_method_name.nil? + key = run_key_transform(link_name) + self.data_links[key] = block || link_method_name + end + + def validate_includes!(includes) + return if includes.blank? + + includes.detect do |include_item| + klass = self + parse_include_item(include_item).each do |parsed_include| + relationship_to_include = klass.relationships_to_serialize[parsed_include] + raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include + raise NotImplementedError if relationship_to_include[:polymorphic].is_a?(Hash) + klass = relationship_to_include[:serializer].to_s.constantize + end + end + end end end end diff --git a/lib/fast_jsonapi/serialization_core.rb b/lib/fast_jsonapi/serialization_core.rb index 3c2e9b7..de138cd 100644 --- a/lib/fast_jsonapi/serialization_core.rb +++ b/lib/fast_jsonapi/serialization_core.rb @@ -4,6 +4,8 @@ require 'active_support/concern' require 'fast_jsonapi/multi_to_json' module FastJsonapi + MandatoryField = Class.new(StandardError) + module SerializationCore extend ActiveSupport::Concern @@ -13,16 +15,23 @@ module FastJsonapi :relationships_to_serialize, :cachable_relationships_to_serialize, :uncachable_relationships_to_serialize, + :transform_method, :record_type, :record_id, :cache_length, - :cached + :race_condition_ttl, + :cached, + :data_links end end class_methods do - def id_hash(id, record_type) - return { id: id.to_s, type: record_type } if id.present? + def id_hash(id, record_type, default_return=false) + if id.present? + { id: id.to_s, type: record_type } + else + default_return ? { id: nil, type: record_type } : nil + end end def ids_hash(ids, record_type) @@ -33,19 +42,18 @@ module FastJsonapi def id_hash_from_record(record, record_types) # memoize the record type within the record_types dictionary, then assigning to record_type: record_type = record_types[record.class] ||= record.class.name.underscore.to_sym - { id: record.id.to_s, type: record_type } + id_hash(record.id, record_type) end - def ids_hash_from_record_and_relationship(record, relationship) + def ids_hash_from_record_and_relationship(record, relationship, params = {}) polymorphic = relationship[:polymorphic] return ids_hash( - record.public_send(relationship[:id_method_name]), + fetch_id(record, relationship, params), relationship[:record_type] ) unless polymorphic - object_method_name = relationship.fetch(:object_method_name, relationship[:name]) - return unless associated_object = record.send(object_method_name) + return unless associated_object = fetch_associated_object(record, relationship, params) return associated_object.map do |object| id_hash_from_record object, polymorphic @@ -54,69 +62,131 @@ module FastJsonapi id_hash_from_record associated_object, polymorphic end - def attributes_hash(record) - attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash| - attr_hash[key] = method.is_a?(Proc) ? method.call(record) : record.public_send(method) + def links_hash(record, params = {}) + data_links.each_with_object({}) do |(key, method), link_hash| + link_hash[key] = if method.is_a?(Proc) + method.arity == 1 ? method.call(record) : method.call(record, params) + else + record.public_send(method) + end end end - def relationships_hash(record, relationships = nil) + def attributes_hash(record, params = {}) + attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash| + attr_hash[key] = if method.is_a?(Proc) + method.arity == 1 ? method.call(record) : method.call(record, params) + else + record.public_send(method) + end + end + end + + def relationships_hash(record, relationships = nil, params = {}) relationships = relationships_to_serialize if relationships.nil? relationships.each_with_object({}) do |(_k, relationship), hash| name = relationship[:key] empty_case = relationship[:relationship_type] == :has_many ? [] : nil hash[name] = { - data: ids_hash_from_record_and_relationship(record, relationship) || empty_case + data: ids_hash_from_record_and_relationship(record, relationship, params) || empty_case } end end - def record_hash(record) + def record_hash(record, params = {}) if cached - record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do - id = record_id ? record.send(record_id) : record.id - temp_hash = id_hash(id, record_type) || { id: nil, type: record_type } - temp_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present? + 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[:relationships] = {} - temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present? + temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, params) if cachable_relationships_to_serialize.present? + temp_hash[:links] = links_hash(record, params) if data_links.present? temp_hash end - record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize)) if uncachable_relationships_to_serialize.present? + record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, params)) if uncachable_relationships_to_serialize.present? record_hash else - id = record_id ? record.send(record_id) : record.id - record_hash = id_hash(id, record_type) || { id: nil, type: record_type } - record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present? - record_hash[:relationships] = relationships_hash(record) if relationships_to_serialize.present? + 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[:links] = links_hash(record, params) if data_links.present? record_hash end end + def id_from_record(record) + return record.send(record_id) if record_id + raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id) + record.id + end + # Override #to_json for alternative implementation def to_json(payload) FastJsonapi::MultiToJson.to_json(payload) if payload.present? end - # includes handler + def parse_include_item(include_item) + return [include_item.to_sym] unless include_item.to_s.include?('.') + include_item.to_s.split('.').map { |item| item.to_sym } + end - def get_included_records(record, includes_list, known_included_objects) - includes_list.each_with_object([]) do |item, included_records| - object_method_name = @relationships_to_serialize[item][:object_method_name] - record_type = @relationships_to_serialize[item][:record_type] - serializer = @relationships_to_serialize[item][:serializer].to_s.constantize - relationship_type = @relationships_to_serialize[item][:relationship_type] - included_objects = record.send(object_method_name) - next if included_objects.blank? - included_objects = [included_objects] unless relationship_type == :has_many - included_objects.each do |inc_obj| - code = "#{record_type}_#{inc_obj.id}" - next if known_included_objects.key?(code) - known_included_objects[code] = inc_obj - included_records << serializer.record_hash(inc_obj) + def remaining_items(items) + return unless items.size > 1 + + items_copy = items.dup + items_copy.delete_at(0) + [items_copy.join('.').to_sym] + end + + # includes handler + def get_included_records(record, includes_list, known_included_objects, params = {}) + return unless includes_list.present? + + includes_list.sort.each_with_object([]) do |include_item, included_records| + items = parse_include_item(include_item) + items.each do |item| + next unless relationships_to_serialize && relationships_to_serialize[item] + raise NotImplementedError if @relationships_to_serialize[item][:polymorphic].is_a?(Hash) + record_type = @relationships_to_serialize[item][:record_type] + serializer = @relationships_to_serialize[item][:serializer].to_s.constantize + relationship_type = @relationships_to_serialize[item][:relationship_type] + + included_objects = fetch_associated_object(record, @relationships_to_serialize[item], params) + next if included_objects.blank? + included_objects = [included_objects] unless relationship_type == :has_many + + included_objects.each do |inc_obj| + if remaining_items(items) + serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects) + included_records.concat(serializer_records) unless serializer_records.empty? + end + + code = "#{record_type}_#{inc_obj.id}" + next if known_included_objects.key?(code) + + known_included_objects[code] = inc_obj + included_records << serializer.record_hash(inc_obj, params) + end end end end + + def fetch_associated_object(record, relationship, params) + return relationship[:object_block].call(record, params) unless relationship[:object_block].nil? + record.send(relationship[:object_method_name]) + end + + def fetch_id(record, relationship, params) + unless relationship[:object_block].nil? + object = relationship[:object_block].call(record, params) + + return object.map(&:id) if object.respond_to? :map + return object.id + end + + record.public_send(relationship[:id_method_name]) + end end end end diff --git a/lib/fast_jsonapi/version.rb b/lib/fast_jsonapi/version.rb new file mode 100644 index 0000000..ecc482c --- /dev/null +++ b/lib/fast_jsonapi/version.rb @@ -0,0 +1,3 @@ +module FastJsonapi + VERSION = "1.1.1" +end diff --git a/spec/lib/instrumentation/skylight/normalizers_require_spec.rb b/spec/lib/instrumentation/skylight/normalizers_require_spec.rb new file mode 100644 index 0000000..8c4fd3b --- /dev/null +++ b/spec/lib/instrumentation/skylight/normalizers_require_spec.rb @@ -0,0 +1,14 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + + context 'instrument' do + context 'skylight' do + # skip for normal runs because this could alter some + # other test by insterting the instrumentation + xit 'make sure requiring skylight normalizers works' do + require 'fast_jsonapi/instrumentation/skylight' + end + end + end +end diff --git a/spec/lib/object_serializer_attribute_param_spec.rb b/spec/lib/object_serializer_attribute_param_spec.rb new file mode 100644 index 0000000..f92b9c7 --- /dev/null +++ b/spec/lib/object_serializer_attribute_param_spec.rb @@ -0,0 +1,118 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + context "params option" do + let(:hash) { serializer.serializable_hash } + + before(:context) do + class Movie + def viewed?(user) + user.viewed.include?(id) + end + end + + class MovieSerializer + attribute :viewed do |movie, params| + params ? movie.viewed?(params[:user]) : false + end + + attribute :no_param_attribute do |movie| + "no-param-attribute" + end + end + + User = Struct.new(:viewed) + end + + after(:context) do + Object.send(:remove_const, User) if Object.constants.include?(User) + end + + context "enforces a hash only params" do + let(:params) { User.new([]) } + + it "fails when creating a serializer with an object as params" do + expect(-> { MovieSerializer.new(movie, {params: User.new([])}) }).to raise_error(ArgumentError) + end + + it "succeeds creating a serializer with a hash" do + expect(-> { MovieSerializer.new(movie, {params: {current_user: User.new([])}}) }).not_to raise_error + end + end + + context "passing params to the serializer" do + let(:params) { {user: User.new([movie.id])} } + let(:options_with_params) { {params: params} } + + context "with a single record" do + let(:serializer) { MovieSerializer.new(movie, options_with_params) } + + it "handles attributes that use params" do + expect(hash[:data][:attributes][:viewed]).to eq(true) + end + + it "handles attributes that don't use params" do + expect(hash[:data][:attributes][:no_param_attribute]).to eq("no-param-attribute") + end + end + + context "with a list of records" do + let(:movies) { build_movies(3) } + let(:user) { User.new(movies.map { |m| [true, false].sample ? m.id : nil }.compact) } + let(:params) { {user: user} } + let(:serializer) { MovieSerializer.new(movies, options_with_params) } + + it "has 3 items" do + hash[:data].length == 3 + end + + it "handles passing params to a list of resources" do + param_attribute_values = hash[:data].map { |data| [data[:id], data[:attributes][:viewed]] } + expected_values = movies.map { |m| [m.id.to_s, user.viewed.include?(m.id)] } + + expect(param_attribute_values).to eq(expected_values) + end + + it "handles attributes without params" do + no_param_attribute_values = hash[:data].map { |data| data[:attributes][:no_param_attribute] } + expected_values = (1..3).map { "no-param-attribute" } + + expect(no_param_attribute_values).to eq(expected_values) + end + end + end + + context "without passing params to the serializer" do + context "with a single movie" do + let(:serializer) { MovieSerializer.new(movie) } + + it "handles param attributes" do + expect(hash[:data][:attributes][:viewed]).to eq(false) + end + + it "handles attributes that don't use params" do + expect(hash[:data][:attributes][:no_param_attribute]).to eq("no-param-attribute") + end + end + + context "with multiple movies" do + let(:serializer) { MovieSerializer.new(build_movies(3)) } + + it "handles attributes with params" do + param_attribute_values = hash[:data].map { |data| data[:attributes][:viewed] } + + expect(param_attribute_values).to eq([false, false, false]) + end + + it "handles attributes that don't use params" do + no_param_attribute_values = hash[:data].map { |data| data[:attributes][:no_param_attribute] } + expected_attribute_values = (1..3).map { "no-param-attribute" } + + expect(no_param_attribute_values).to eq(expected_attribute_values) + end + end + end + end +end diff --git a/spec/lib/object_serializer_caching_spec.rb b/spec/lib/object_serializer_caching_spec.rb index de0186a..8e455d4 100644 --- a/spec/lib/object_serializer_caching_spec.rb +++ b/spec/lib/object_serializer_caching_spec.rb @@ -13,6 +13,8 @@ describe FastJsonapi::ObjectSerializer do it 'returns correct hash when serializable_hash is called' do options = {} options[:meta] = { total: 2 } + options[:links] = { self: 'self' } + options[:include] = [:actors] serializable_hash = CachingMovieSerializer.new([movie, movie], options).serializable_hash @@ -21,6 +23,7 @@ describe FastJsonapi::ObjectSerializer do expect(serializable_hash[:data][0][:attributes].length).to eq 2 expect(serializable_hash[:meta]).to be_instance_of(Hash) + expect(serializable_hash[:links]).to be_instance_of(Hash) expect(serializable_hash[:included]).to be_instance_of(Array) expect(serializable_hash[:included][0]).to be_instance_of(Hash) @@ -30,6 +33,7 @@ describe FastJsonapi::ObjectSerializer do expect(serializable_hash[:data]).to be_instance_of(Hash) expect(serializable_hash[:meta]).to be nil + expect(serializable_hash[:links]).to be nil expect(serializable_hash[:included]).to be nil end diff --git a/spec/lib/object_serializer_class_methods_spec.rb b/spec/lib/object_serializer_class_methods_spec.rb index 4d23c2b..55e351a 100644 --- a/spec/lib/object_serializer_class_methods_spec.rb +++ b/spec/lib/object_serializer_class_methods_spec.rb @@ -4,73 +4,342 @@ describe FastJsonapi::ObjectSerializer do include_context 'movie class' - context 'when testing class methods of object serializer' do + describe '#has_many' do + subject(:relationship) { serializer.relationships_to_serialize[:roles] } - before(:example) do + before do + serializer.has_many *children + end + + after do + serializer.relationships_to_serialize = {} + end + + context 'with namespace' do + let(:serializer) { AppName::V1::MovieSerializer } + let(:children) { [:roles] } + + context 'with overrides' do + let(:children) { [:roles, id_method_name: :roles_only_ids, record_type: :super_role] } + + it_behaves_like 'returning correct relationship hash', :'AppName::V1::RoleSerializer', :roles_only_ids, :super_role + end + + context 'without overrides' do + let(:children) { [:roles] } + + it_behaves_like 'returning correct relationship hash', :'AppName::V1::RoleSerializer', :role_ids, :role + end + end + + context 'without namespace' do + let(:serializer) { MovieSerializer } + + context 'with overrides' do + let(:children) { [:roles, id_method_name: :roles_only_ids, record_type: :super_role] } + + it_behaves_like 'returning correct relationship hash', :'RoleSerializer', :roles_only_ids, :super_role + end + + context 'without overrides' do + let(:children) { [:roles] } + + it_behaves_like 'returning correct relationship hash', :'RoleSerializer', :role_ids, :role + end + end + end + + describe '#has_many with block' do + before do + MovieSerializer.has_many :awards do |movie| + movie.actors.map(&:awards).flatten + end + end + + after do + MovieSerializer.relationships_to_serialize.delete(:awards) + end + + context 'awards is not included' do + subject(:hash) { MovieSerializer.new(movie).serializable_hash } + + it 'returns correct hash' do + expect(hash[:data][:relationships][:awards][:data].length).to eq(6) + expect(hash[:data][:relationships][:awards][:data][0]).to eq({ id: '9', type: :award }) + expect(hash[:data][:relationships][:awards][:data][-1]).to eq({ id: '28', type: :award }) + end + end + + context 'state is included' do + subject(:hash) { MovieSerializer.new(movie, include: [:awards]).serializable_hash } + + it 'returns correct hash' do + expect(hash[:included].length).to eq 6 + expect(hash[:included][0][:id]).to eq '9' + expect(hash[:included][0][:type]).to eq :award + expect(hash[:included][0][:attributes]).to eq({ id: 9, title: 'Test Award 9' }) + expect(hash[:included][0][:relationships]).to eq({ actor: { data: { id: '1', type: :actor } } }) + expect(hash[:included][-1][:id]).to eq '28' + expect(hash[:included][-1][:type]).to eq :award + expect(hash[:included][-1][:attributes]).to eq({ id: 28, title: 'Test Award 28' }) + expect(hash[:included][-1][:relationships]).to eq({ actor: { data: { id: '3', type: :actor } } }) + end + end + end + + describe '#belongs_to' do + subject(:relationship) { MovieSerializer.relationships_to_serialize[:area] } + + before do + MovieSerializer.belongs_to *parent + end + + after do MovieSerializer.relationships_to_serialize = {} end - it 'returns correct relationship hash for a has_many relationship' do - MovieSerializer.has_many :roles - relationship = MovieSerializer.relationships_to_serialize[:roles] - expect(relationship).to be_instance_of(Hash) - expect(relationship.keys).to all(be_instance_of(Symbol)) - expect(relationship[:id_method_name]).to end_with '_ids' - expect(relationship[:record_type]).to eq 'roles'.singularize.to_sym + context 'with overrides' do + let(:parent) { [:area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area] } + + it_behaves_like 'returning correct relationship hash', :'MyAreaSerializer', :blah_id, :awesome_area end - it 'returns correct relationship hash for a has_many relationship with overrides' do - MovieSerializer.has_many :roles, id_method_name: :roles_only_ids, record_type: :super_role - relationship = MovieSerializer.relationships_to_serialize[:roles] - expect(relationship[:id_method_name]).to be :roles_only_ids - expect(relationship[:record_type]).to be :super_role + context 'without overrides' do + let(:parent) { [:area] } + + it_behaves_like 'returning correct relationship hash', :'AreaSerializer', :area_id, :area + end + end + + describe '#belongs_to with block' do + before do + ActorSerializer.belongs_to :state do |actor| + actor.agency.state + end end - it 'returns correct relationship hash for a belongs_to relationship' do - MovieSerializer.belongs_to :area - relationship = MovieSerializer.relationships_to_serialize[:area] - expect(relationship).to be_instance_of(Hash) - expect(relationship.keys).to all(be_instance_of(Symbol)) - expect(relationship[:id_method_name]).to end_with '_id' - expect(relationship[:record_type]).to eq 'area'.singularize.to_sym + after do + ActorSerializer.relationships_to_serialize.delete(:actorc) end - it 'returns correct relationship hash for a belongs_to relationship with overrides' do - MovieSerializer.has_many :area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area - relationship = MovieSerializer.relationships_to_serialize[:area] - expect(relationship[:id_method_name]).to be :blah_id - expect(relationship[:record_type]).to be :awesome_area - expect(relationship[:serializer]).to be :MyAreaSerializer + context 'state is not included' do + subject(:hash) { ActorSerializer.new(actor).serializable_hash } + + it 'returns correct hash' do + expect(hash[:data][:relationships][:state][:data]).to eq({ id: '1', type: :state }) + end end - it 'returns correct relationship hash for a has_one relationship' do - MovieSerializer.has_one :area - relationship = MovieSerializer.relationships_to_serialize[:area] - expect(relationship).to be_instance_of(Hash) - expect(relationship.keys).to all(be_instance_of(Symbol)) - expect(relationship[:id_method_name]).to end_with '_id' - expect(relationship[:record_type]).to eq 'area'.singularize.to_sym + context 'state is included' do + subject(:hash) { ActorSerializer.new(actor, include: [:state]).serializable_hash } + + it 'returns correct hash' do + expect(hash[:included].length).to eq 1 + expect(hash[:included][0][:id]).to eq '1' + expect(hash[:included][0][:type]).to eq :state + expect(hash[:included][0][:attributes]).to eq({ id: 1, name: 'Test State 1' }) + expect(hash[:included][0][:relationships]).to eq({ agency: { data: [{ id: '432', type: :agency }] } }) + end + end + end + + describe '#has_one' do + subject(:relationship) { MovieSerializer.relationships_to_serialize[:area] } + + before do + MovieSerializer.has_one *partner end - it 'returns correct relationship hash for a has_one relationship with overrides' do - MovieSerializer.has_one :area, id_method_name: :blah_id, record_type: :awesome_area - relationship = MovieSerializer.relationships_to_serialize[:area] - expect(relationship[:id_method_name]).to be :blah_id - expect(relationship[:record_type]).to be :awesome_area + after do + MovieSerializer.relationships_to_serialize = {} end - it 'returns serializer name correctly with namespaces' do - AppName::V1::MovieSerializer.has_many :area, id_method_name: :blah_id - relationship = AppName::V1::MovieSerializer.relationships_to_serialize[:area] - expect(relationship[:serializer]).to be :'AppName::V1::AreaSerializer' + context 'with overrides' do + let(:partner) { [:area, id_method_name: :blah_id, record_type: :awesome_area, serializer: :my_area] } + + it_behaves_like 'returning correct relationship hash', :'MyAreaSerializer', :blah_id, :awesome_area + end + + context 'without overrides' do + let(:partner) { [:area] } + + it_behaves_like 'returning correct relationship hash', :'AreaSerializer', :area_id, :area + end + end + + describe '#set_id' do + subject(:serializable_hash) { MovieSerializer.new(resource).serializable_hash } + + before do + MovieSerializer.set_id :owner_id + end + + after do + MovieSerializer.set_id nil + end + + context 'when one record is given' do + let(:resource) { movie } + + it 'returns correct hash which id equals owner_id' do + expect(serializable_hash[:data][:id].to_i).to eq movie.owner_id + end + end + + context 'when an array of records is given' do + let(:resource) { [movie, movie] } + + it 'returns correct hash which id equals owner_id' do + expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id + expect(serializable_hash[:data][1][:id].to_i).to eq movie.owner_id + end + end + end + + describe '#use_hyphen' do + subject { MovieSerializer.use_hyphen } + + after do + MovieSerializer.transform_method = nil end it 'sets the correct transform_method when use_hyphen is used' do - MovieSerializer.use_hyphen - warning_message = 'DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead' - expect { MovieSerializer.use_hyphen }.to output.to_stderr + warning_message = "DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead\n" + expect { subject }.to output(warning_message).to_stderr expect(MovieSerializer.instance_variable_get(:@transform_method)).to eq :dasherize end end + describe '#attribute' do + subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash } + + context 'with block' do + before do + movie.release_year = 2008 + MovieSerializer.attribute :title_with_year do |record| + "#{record.name} (#{record.release_year})" + end + end + + after do + MovieSerializer.attributes_to_serialize.delete(:title_with_year) + end + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:attributes][:name]).to eq movie.name + expect(serializable_hash[:data][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})" + end + end + end + + describe '#link' do + subject(:serializable_hash) { MovieSerializer.new(movie).serializable_hash } + + after do + MovieSerializer.data_links = {} + ActorSerializer.data_links = {} + end + + context 'with block calling instance method on serializer' do + before do + MovieSerializer.link(:self) do |movie_object| + movie_object.url + end + end + let(:url) { "http://movies.com/#{movie.id}" } + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:links][:self]).to eq url + end + end + + context 'with block and param' do + before do + MovieSerializer.link(:public_url) do |movie_object| + "http://movies.com/#{movie_object.id}" + end + end + let(:url) { "http://movies.com/#{movie.id}" } + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:links][:public_url]).to eq url + end + end + + context 'with method' do + before do + MovieSerializer.link(:object_id, :id) + end + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:links][:object_id]).to eq movie.id + end + end + + context 'with method and convention' do + before do + MovieSerializer.link(:url) + end + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:data][:links][:url]).to eq movie.url + end + end + end + + describe '#key_transform' do + subject(:hash) { movie_serializer_class.new([movie, movie], include: [:movie_type]).serializable_hash } + + let(:movie_serializer_class) { "#{key_transform}_movie_serializer".classify.constantize } + + before(:context) do + [:dash, :camel, :camel_lower, :underscore].each do |key_transform| + movie_serializer_name = "#{key_transform}_movie_serializer".classify + movie_type_serializer_name = "#{key_transform}_movie_type_serializer".classify + # https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name + movie_serializer_class = Object.const_set(movie_serializer_name, Class.new) + # https://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent/chapters/24-eval/lessons/67-instance-eval + movie_serializer_class.instance_eval do + include FastJsonapi::ObjectSerializer + set_type :movie + set_key_transform key_transform + attributes :name, :release_year + has_many :actors + belongs_to :owner, record_type: :user + belongs_to :movie_type, serializer: "#{key_transform}_movie_type".to_sym + end + movie_type_serializer_class = Object.const_set(movie_type_serializer_name, Class.new) + movie_type_serializer_class.instance_eval do + include FastJsonapi::ObjectSerializer + set_key_transform key_transform + set_type :movie_type + attributes :name + end + end + end + + context 'when key_transform is dash' do + let(:key_transform) { :dash } + + it_behaves_like 'returning key transformed hash', :'movie-type', :'release-year' + end + + context 'when key_transform is camel' do + let(:key_transform) { :camel } + + it_behaves_like 'returning key transformed hash', :MovieType, :ReleaseYear + end + + context 'when key_transform is camel_lower' do + let(:key_transform) { :camel_lower } + + it_behaves_like 'returning key transformed hash', :movieType, :releaseYear + end + + context 'when key_transform is underscore' do + let(:key_transform) { :underscore } + + it_behaves_like 'returning key transformed hash', :movie_type, :release_year + end + end end diff --git a/spec/lib/object_serializer_inheritance_spec.rb b/spec/lib/object_serializer_inheritance_spec.rb new file mode 100644 index 0000000..06dba25 --- /dev/null +++ b/spec/lib/object_serializer_inheritance_spec.rb @@ -0,0 +1,163 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + + after(:all) do + classes_to_remove = %i[ + User + UserSerializer + Country + CountrySerializer + Employee + EmployeeSerializer + Photo + PhotoSerializer + EmployeeAccount + ] + classes_to_remove.each do |klass_name| + Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) + end + end + + class User + attr_accessor :id, :first_name, :last_name + + attr_accessor :address_ids, :country_id + + def photo + p = Photo.new + p.id = 1 + p.user_id = id + p + end + + def photo_id + 1 + end + end + + class UserSerializer + include FastJsonapi::ObjectSerializer + set_type :user + attributes :first_name, :last_name + + attribute :full_name do |user, params| + "#{user.first_name} #{user.last_name}" + end + + has_many :addresses, cached: true + belongs_to :country + has_one :photo + end + + class Photo + attr_accessor :id, :user_id + end + + class PhotoSerializer + include FastJsonapi::ObjectSerializer + attributes :id, :name + end + + class Country + attr_accessor :id, :name + end + + class CountrySerializer + include FastJsonapi::ObjectSerializer + attributes :name + end + + class EmployeeAccount + attr_accessor :id, :employee_id + end + + class Employee < User + attr_accessor :id, :location, :compensation + + def account + a = EmployeeAccount.new + a.id = 1 + a.employee_id = id + a + end + + def account_id + 1 + end + end + + class EmployeeSerializer < UserSerializer + include FastJsonapi::ObjectSerializer + attributes :location + attributes :compensation + + has_one :account + 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 + expect(subclass_attributes).to include(superclass_attributes) + end + + it 'returns inherited attribute with a block correctly' do + e = Employee.new + e.id = 1 + e.first_name = 'S' + e.last_name = 'K' + attributes_hash = EmployeeSerializer.new(e).serializable_hash[:data][:attributes] + expect(attributes_hash).to include(full_name: 'S K') + end + + it 'includes child attributes' do + expect(EmployeeSerializer.attributes_to_serialize[:location]).to eq(:location) + end + + it 'doesnt change parent class attributes' do + EmployeeSerializer + expect(UserSerializer.attributes_to_serialize).not_to have_key(:location) + end + end + + context 'when testing inheritance of relationship' do + it 'includes parent relationships' do + subclass_relationships = EmployeeSerializer.relationships_to_serialize + superclass_relationships = UserSerializer.relationships_to_serialize + expect(subclass_relationships).to include(superclass_relationships) + end + + it 'returns inherited relationship correctly' do + e = Employee.new + e.country_id = 1 + relationships_hash = EmployeeSerializer.new(e).serializable_hash[:data][:relationships][:country] + expect(relationships_hash).to include(data: { id: "1", type: :country }) + end + + it 'includes child relationships' do + expect(EmployeeSerializer.relationships_to_serialize.keys).to include(:account) + end + + it 'doesnt change parent class attributes' do + EmployeeSerializer + expect(UserSerializer.relationships_to_serialize.keys).not_to include(:account) + end + + it 'includes parent cached relationships' do + subclass_relationships = EmployeeSerializer.cachable_relationships_to_serialize + superclass_relationships = UserSerializer.cachable_relationships_to_serialize + expect(subclass_relationships).to include(superclass_relationships) + end + end + + context 'when test inheritence of other attributes' do + + it 'inherits the tranform method' do + EmployeeSerializer + expect(UserSerializer.transform_method).to eq EmployeeSerializer.transform_method + end + + end +end diff --git a/spec/lib/object_serializer_key_transform_spec.rb b/spec/lib/object_serializer_key_transform_spec.rb deleted file mode 100644 index 77338b1..0000000 --- a/spec/lib/object_serializer_key_transform_spec.rb +++ /dev/null @@ -1,80 +0,0 @@ -require 'spec_helper' - -describe FastJsonapi::ObjectSerializer do - include_context 'movie class' - include_context 'ams movie class' - - before(:context) do - [:dash, :camel, :camel_lower, :underscore].each do |transform_type| - movie_serializer_name = "#{transform_type}_movie_serializer".classify - movie_type_serializer_name = "#{transform_type}_movie_type_serializer".classify - # https://stackoverflow.com/questions/4113479/dynamic-class-definition-with-a-class-name - movie_serializer_class = Object.const_set( - movie_serializer_name, - Class.new { - } - ) - # https://rubymonk.com/learning/books/5-metaprogramming-ruby-ascent/chapters/24-eval/lessons/67-instance-eval - movie_serializer_class.instance_eval do - include FastJsonapi::ObjectSerializer - set_type :movie - set_key_transform transform_type - attributes :name, :release_year - has_many :actors - belongs_to :owner, record_type: :user - belongs_to :movie_type - end - movie_type_serializer_class = Object.const_set( - movie_type_serializer_name, - Class.new { - } - ) - movie_type_serializer_class.instance_eval do - include FastJsonapi::ObjectSerializer - set_key_transform transform_type - set_type :movie_type - attributes :name - end - end - end - - context 'when using dashes for word separation in the JSON API members' do - it 'returns correct hash when serializable_hash is called' do - serializable_hash = DashMovieSerializer.new([movie, movie]).serializable_hash - expect(serializable_hash[:data].length).to eq 2 - expect(serializable_hash[:data][0][:relationships].length).to eq 3 - expect(serializable_hash[:data][0][:relationships]).to have_key('movie-type'.to_sym) - expect(serializable_hash[:data][0][:attributes].length).to eq 2 - expect(serializable_hash[:data][0][:attributes]).to have_key("release-year".to_sym) - - serializable_hash = DashMovieSerializer.new(movie_struct).serializable_hash - expect(serializable_hash[:data][:relationships].length).to eq 3 - expect(serializable_hash[:data][:relationships]).to have_key('movie-type'.to_sym) - expect(serializable_hash[:data][:attributes].length).to eq 2 - expect(serializable_hash[:data][:attributes]).to have_key('release-year'.to_sym) - expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s - end - - it 'returns type hypenated when trying to serializing a class with multiple words' do - movie_type = MovieType.new - movie_type.id = 3 - movie_type.name = "x" - serializable_hash = DashMovieTypeSerializer.new(movie_type).serializable_hash - expect(serializable_hash[:data][:type].to_sym).to eq 'movie-type'.to_sym - end - end - - context 'when using other key transforms' do - [:camel, :camel_lower, :underscore, :dash].each do |transform_type| - it "returns same thing as ams when using #{transform_type}" do - ams_movie = build_ams_movies(1).first - movie = build_movies(1).first - movie_serializer_class = "#{transform_type}_movie_serializer".classify.constantize - our_json = movie_serializer_class.new([movie]).serialized_json - ams_json = ActiveModelSerializers::SerializableResource.new([ams_movie], key_transform: transform_type).to_json - expect(our_json.length).to eq (ams_json.length) - end - end - end - -end diff --git a/spec/lib/object_serializer_performance_spec.rb b/spec/lib/object_serializer_performance_spec.rb index 8e52dd2..9119ce3 100644 --- a/spec/lib/object_serializer_performance_spec.rb +++ b/spec/lib/object_serializer_performance_spec.rb @@ -137,7 +137,6 @@ describe FastJsonapi::ObjectSerializer, performance: true do # json expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time] - expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] # hash hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time] @@ -174,7 +173,6 @@ describe FastJsonapi::ObjectSerializer, performance: true do # json expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time] - expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] # hash hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time] @@ -209,7 +207,6 @@ describe FastJsonapi::ObjectSerializer, performance: true do # json expect(json_benchmarks[:fast_jsonapi][:json].length).to eq json_benchmarks[:ams][:json].length json_speed_up = json_benchmarks[:ams][:time] / json_benchmarks[:fast_jsonapi][:time] - expect(json_speed_up).to be >= SERIALIZERS[:ams][:speed_factor] # hash hash_speed_up = hash_benchmarks[:ams][:time] / hash_benchmarks[:fast_jsonapi][:time] diff --git a/spec/lib/object_serializer_relationship_param_spec.rb b/spec/lib/object_serializer_relationship_param_spec.rb new file mode 100644 index 0000000..d016814 --- /dev/null +++ b/spec/lib/object_serializer_relationship_param_spec.rb @@ -0,0 +1,63 @@ +require 'spec_helper' + +describe FastJsonapi::ObjectSerializer do + include_context 'movie class' + + context "params option" do + let(:hash) { serializer.serializable_hash } + + before(:context) do + class MovieSerializer + has_many :agencies do |movie, params| + movie.actors.map(&:agency) if params[:authorized] + end + + belongs_to :primary_agency do |movie, params| + movie.actors.map(&:agency)[0] if params[:authorized] + end + + belongs_to :secondary_agency do |movie| + movie.actors.map(&:agency)[1] + end + end + end + + context "passing params to the serializer" do + let(:params) { {authorized: true} } + let(:options_with_params) { {params: params} } + + 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) + expect(ids).to eq [0,1,2] + end + + it "handles relationships that don't use params" do + expect(hash[:data][:relationships][:secondary_agency][:data]).to include({id: 1.to_s}) + end + end + + context "with a list of records" do + let(:movies) { build_movies(3) } + let(:params) { {authorized: true} } + let(:serializer) { MovieSerializer.new(movies, options_with_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 + expect(relationships_hashes.map{|a| a[:id].to_i}).to contain_exactly 0,1,2 + + uniq_count = hash[:data].map{|a| a[:relationships][:primary_agency] }.uniq.count + expect(uniq_count).to eq 1 + end + + it "handles relationships without params" do + uniq_count = hash[:data].map{|a| a[:relationships][:secondary_agency] }.uniq.count + expect(uniq_count).to eq 1 + end + end + end + end +end diff --git a/spec/lib/object_serializer_set_id_spec.rb b/spec/lib/object_serializer_set_id_spec.rb deleted file mode 100644 index 2f731fd..0000000 --- a/spec/lib/object_serializer_set_id_spec.rb +++ /dev/null @@ -1,30 +0,0 @@ -require 'spec_helper' - -describe FastJsonapi::ObjectSerializer do - include_context 'movie class' - - context 'when setting id' do - subject(:serializable_hash) { MovieSerializer.new(resource).serializable_hash } - - before(:all) do - MovieSerializer.set_id :owner_id - end - - context 'when one record is given' do - let(:resource) { movie } - - it 'returns correct hash which id equals owner_id' do - expect(serializable_hash[:data][:id].to_i).to eq movie.owner_id - end - end - - context 'when an array of records is given' do - let(:resource) { [movie, movie] } - - it 'returns correct hash which id equals owner_id' do - expect(serializable_hash[:data][0][:id].to_i).to eq movie.owner_id - expect(serializable_hash[:data][1][:id].to_i).to eq movie.owner_id - end - end - end -end diff --git a/spec/lib/object_serializer_spec.rb b/spec/lib/object_serializer_spec.rb index 83e1eee..2d8c99e 100644 --- a/spec/lib/object_serializer_spec.rb +++ b/spec/lib/object_serializer_spec.rb @@ -2,19 +2,22 @@ require 'spec_helper' describe FastJsonapi::ObjectSerializer do include_context 'movie class' + include_context 'group class' context 'when testing instance methods of object serializer' do it 'returns correct hash when serializable_hash is called' do options = {} options[:meta] = { total: 2 } + options[:links] = { self: 'self' } options[:include] = [:actors] serializable_hash = MovieSerializer.new([movie, movie], options).serializable_hash expect(serializable_hash[:data].length).to eq 2 - expect(serializable_hash[:data][0][:relationships].length).to eq 3 + expect(serializable_hash[:data][0][:relationships].length).to eq 4 expect(serializable_hash[:data][0][:attributes].length).to eq 2 expect(serializable_hash[:meta]).to be_instance_of(Hash) + expect(serializable_hash[:links]).to be_instance_of(Hash) expect(serializable_hash[:included]).to be_instance_of(Array) expect(serializable_hash[:included][0]).to be_instance_of(Hash) @@ -24,9 +27,34 @@ describe FastJsonapi::ObjectSerializer do expect(serializable_hash[:data]).to be_instance_of(Hash) expect(serializable_hash[:meta]).to be nil + expect(serializable_hash[:links]).to be nil expect(serializable_hash[:included]).to be nil end + it 'returns correct nested includes when serializable_hash is called' do + # 3 actors, 3 agencies + include_object_total = 6 + + options = {} + options[:include] = [:actors, :'actors.agency'] + serializable_hash = MovieSerializer.new([movie], options).serializable_hash + + expect(serializable_hash[:included]).to be_instance_of(Array) + expect(serializable_hash[:included].length).to eq include_object_total + (0..include_object_total-1).each do |include| + expect(serializable_hash[:included][include]).to be_instance_of(Hash) + end + + options[:include] = [:'actors.agency'] + serializable_hash = MovieSerializer.new([movie], options).serializable_hash + + expect(serializable_hash[:included]).to be_instance_of(Array) + expect(serializable_hash[:included].length).to eq include_object_total + (0..include_object_total-1).each do |include| + expect(serializable_hash[:included][include]).to be_instance_of(Hash) + end + end + it 'returns correct number of records when serialized_json is called for an array' do options = {} options[:meta] = { total: 2 } @@ -123,6 +151,107 @@ describe FastJsonapi::ObjectSerializer do end end + context 'nested includes' do + it 'has_many to belongs_to: returns correct nested includes when serializable_hash is called' do + # 3 actors, 3 agencies + include_object_total = 6 + + options = {} + options[:include] = [:actors, :'actors.agency'] + serializable_hash = MovieSerializer.new([movie], options).serializable_hash + + expect(serializable_hash[:included]).to be_instance_of(Array) + expect(serializable_hash[:included].length).to eq include_object_total + (0..include_object_total-1).each do |include| + expect(serializable_hash[:included][include]).to be_instance_of(Hash) + end + + options[:include] = [:'actors.agency'] + serializable_hash = MovieSerializer.new([movie], options).serializable_hash + + expect(serializable_hash[:included]).to be_instance_of(Array) + expect(serializable_hash[:included].length).to eq include_object_total + (0..include_object_total-1).each do |include| + expect(serializable_hash[:included][include]).to be_instance_of(Hash) + end + end + + it '`has_many` to `belongs_to` to `belongs_to` - returns correct nested includes when serializable_hash is called' do + # 3 actors, 3 agencies, 1 state + include_object_total = 7 + + options = {} + options[:include] = [:actors, :'actors.agency', :'actors.agency.state'] + serializable_hash = MovieSerializer.new([movie], options).serializable_hash + + expect(serializable_hash[:included]).to be_instance_of(Array) + expect(serializable_hash[:included].length).to eq include_object_total + + actors_serialized = serializable_hash[:included].find_all { |included| included[:type] == :actor }.map { |included| included[:id].to_i } + agencies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :agency }.map { |included| included[:id].to_i } + states_serialized = serializable_hash[:included].find_all { |included| included[:type] == :state }.map { |included| included[:id].to_i } + + movie.actors.each do |actor| + expect(actors_serialized).to include(actor.id) + end + + agencies = movie.actors.map(&:agency).uniq + agencies.each do |agency| + expect(agencies_serialized).to include(agency.id) + end + + states = agencies.map(&:state).uniq + states.each do |state| + expect(states_serialized).to include(state.id) + end + end + + it 'has_many => has_one returns correct nested includes when serializable_hash is called' do + options = {} + options[:include] = [:movies, :'movies.advertising_campaign'] + serializable_hash = MovieTypeSerializer.new([movie_type], options).serializable_hash + + movies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :movie }.map { |included| included[:id].to_i } + advertising_campaigns_serialized = serializable_hash[:included].find_all { |included| included[:type] == :advertising_campaign }.map { |included| included[:id].to_i } + + movies = movie_type.movies + movies.each do |movie| + expect(movies_serialized).to include(movie.id) + end + + advertising_campaigns = movies.map(&:advertising_campaign) + advertising_campaigns.each do |advertising_campaign| + expect(advertising_campaigns_serialized).to include(advertising_campaign.id) + end + end + + it 'belongs_to: returns correct nested includes when nested attributes are nil when serializable_hash is called' do + class Movie + def advertising_campaign + nil + end + end + + options = {} + options[:include] = [:movies, :'movies.advertising_campaign'] + + serializable_hash = MovieTypeSerializer.new([movie_type], options).serializable_hash + + movies_serialized = serializable_hash[:included].find_all { |included| included[:type] == :movie }.map { |included| included[:id].to_i } + + movies = movie_type.movies + movies.each do |movie| + expect(movies_serialized).to include(movie.id) + end + end + + it 'polymorphic throws an error that polymorphic is not supported' do + options = {} + options[:include] = [:groupees] + expect(-> { GroupSerializer.new([group], options)}).to raise_error(NotImplementedError) + end + end + context 'when testing included do block of object serializer' do it 'should set default_type based on serializer class name' do class BlahSerializer @@ -154,4 +283,23 @@ describe FastJsonapi::ObjectSerializer do expect(V1::BlahSerializer.record_type).to be :blah end end + + context 'when serializing included, serialize any links' do + before do + ActorSerializer.link(:self) do |actor_object| + actor_object.url + end + end + subject(:serializable_hash) do + options = {} + options[:include] = [:actors] + MovieSerializer.new(movie, options).serializable_hash + end + let(:actor) { movie.actors.first } + let(:url) { "http://movies.com/actors/#{actor.id}" } + + it 'returns correct hash when serializable_hash is called' do + expect(serializable_hash[:included][0][:links][:self]).to eq url + end + end end diff --git a/spec/lib/object_serializer_struct_spec.rb b/spec/lib/object_serializer_struct_spec.rb index cb9054b..cf703ec 100644 --- a/spec/lib/object_serializer_struct_spec.rb +++ b/spec/lib/object_serializer_struct_spec.rb @@ -7,14 +7,16 @@ describe FastJsonapi::ObjectSerializer do it 'returns correct hash when serializable_hash is called' do options = {} options[:meta] = { total: 2 } + options[:links] = { self: 'self' } options[:include] = [:actors] serializable_hash = MovieSerializer.new([movie_struct, movie_struct], options).serializable_hash expect(serializable_hash[:data].length).to eq 2 - expect(serializable_hash[:data][0][:relationships].length).to eq 3 + expect(serializable_hash[:data][0][:relationships].length).to eq 4 expect(serializable_hash[:data][0][:attributes].length).to eq 2 expect(serializable_hash[:meta]).to be_instance_of(Hash) + expect(serializable_hash[:links]).to be_instance_of(Hash) expect(serializable_hash[:included]).to be_instance_of(Array) expect(serializable_hash[:included][0]).to be_instance_of(Hash) @@ -24,8 +26,16 @@ describe FastJsonapi::ObjectSerializer do expect(serializable_hash[:data]).to be_instance_of(Hash) expect(serializable_hash[:meta]).to be nil + expect(serializable_hash[:links]).to be nil expect(serializable_hash[:included]).to be nil expect(serializable_hash[:data][:id]).to eq movie_struct.id.to_s end + + context 'struct without id' do + it 'returns correct hash when serializable_hash is called' do + serializer = MovieWithoutIdStructSerializer.new(movie_struct_without_id) + expect { serializer.serializable_hash }.to raise_error(FastJsonapi::MandatoryField) + end + end end end diff --git a/spec/lib/object_serializer_with_attribute_block_spec.rb b/spec/lib/object_serializer_with_attribute_block_spec.rb deleted file mode 100644 index 885cae0..0000000 --- a/spec/lib/object_serializer_with_attribute_block_spec.rb +++ /dev/null @@ -1,13 +0,0 @@ -require 'spec_helper' - -describe FastJsonapi::ObjectSerializer do - include_context 'movie class' - - context 'when including attribute blocks' do - it 'returns correct hash when serializable_hash is called' do - serializable_hash = MovieSerializerWithAttributeBlock.new([movie]).serializable_hash - expect(serializable_hash[:data][0][:attributes][:name]).to eq movie.name - expect(serializable_hash[:data][0][:attributes][:title_with_year]).to eq "#{movie.name} (#{movie.release_year})" - end - end -end diff --git a/spec/lib/serialization_core_spec.rb b/spec/lib/serialization_core_spec.rb index 2d327d4..64f25bb 100644 --- a/spec/lib/serialization_core_spec.rb +++ b/spec/lib/serialization_core_spec.rb @@ -18,7 +18,7 @@ describe FastJsonapi::ObjectSerializer do end it 'returns the correct hash when ids_hash_from_record_and_relationship is called for a polymorphic association' do - relationship = { name: :groupees, relationship_type: :has_many, polymorphic: {} } + relationship = { name: :groupees, relationship_type: :has_many, object_method_name: :groupees, polymorphic: {} } results = GroupSerializer.send :ids_hash_from_record_and_relationship, group, relationship expect(results).to include({ id: "1", type: :person }, { id: "2", type: :group }) end @@ -70,7 +70,7 @@ describe FastJsonapi::ObjectSerializer do end it 'returns correct hash when record_hash is called' do - record_hash = MovieSerializer.send(:record_hash, movie) + record_hash = MovieSerializer.send(:record_hash, movie, nil) expect(record_hash[:id]).to eq movie.id.to_s expect(record_hash[:type]).to eq MovieSerializer.record_type expect(record_hash).to have_key(:attributes) if MovieSerializer.attributes_to_serialize.present? @@ -82,7 +82,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) + 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/ams_context.rb b/spec/shared/contexts/ams_context.rb index 4683223..f4cf01e 100644 --- a/spec/shared/contexts/ams_context.rb +++ b/spec/shared/contexts/ams_context.rb @@ -4,12 +4,55 @@ RSpec.shared_context 'ams movie class' do class AMSModel < ActiveModelSerializers::Model derive_attributes_from_names_and_fix_accessors end + + class AMSMovieType < AMSModel + attributes :id, :name, :movies + end class AMSMovie < AMSModel - attributes :id, :name, :release_year, :actors, :owner, :movie_type + attributes :id, :name, :release_year, :actors, :owner, :movie_type, :advertising_campaign + + def movie_type + mt = AMSMovieType.new + mt.id = 1 + mt.name = 'Episode' + mt.movies = [self] + mt + end + end + + class AMSAdvertisingCampaign < AMSModel + attributes :id, :name, :movie + end + + class AMSAward < AMSModel + attributes :id, :title, :actor + end + + class AMSAgency < AMSModel + attributes :id, :name, :actors end class AMSActor < AMSModel - attributes :id, :name, :email + attributes :id, :name, :email, :agency, :awards, :agency_id + def agency + AMSAgency.new.tap do |a| + a.id = agency_id + a.name = "Test Agency #{agency_id}" + end + end + + def award_ids + [id * 9, id * 9 + 1] + end + + def awards + award_ids.map do |i| + AMSAward.new.tap do |a| + a.id = i + a.title = "Test Award #{i}" + end + end + end end class AMSUser < AMSModel @@ -19,9 +62,22 @@ RSpec.shared_context 'ams movie class' do attributes :id, :name end # serializers + class AMSAwardSerializer < ActiveModel::Serializer + type 'award' + attributes :id, :title + belongs_to :actor + end + class AMSAgencySerializer < ActiveModel::Serializer + type 'agency' + attributes :id, :name + belongs_to :state + has_many :actors + end class AMSActorSerializer < ActiveModel::Serializer type 'actor' attributes :name, :email + belongs_to :agency, serializer: ::AMSAgencySerializer + has_many :awards, serializer: ::AMSAwardSerializer end class AMSUserSerializer < ActiveModel::Serializer type 'user' @@ -30,6 +86,11 @@ RSpec.shared_context 'ams movie class' do class AMSMovieTypeSerializer < ActiveModel::Serializer type 'movie_type' attributes :name + has_many :movies + end + class AMSAdvertisingCampaignSerializer < ActiveModel::Serializer + type 'advertising_campaign' + attributes :name end class AMSMovieSerializer < ActiveModel::Serializer type 'movie' @@ -37,6 +98,7 @@ RSpec.shared_context 'ams movie class' do has_many :actors has_one :owner belongs_to :movie_type + has_one :advertising_campaign end end @@ -53,6 +115,7 @@ RSpec.shared_context 'ams movie class' do a.id = i + 1 a.name = "Test #{a.id}" a.email = "test#{a.id}@test.com" + a.agency_id = i a end end @@ -70,6 +133,13 @@ RSpec.shared_context 'ams movie class' do ams_movie_type end + let(:ams_advertising_campaign) do + campaign = AMSAdvertisingCampaign.new + campaign.id = 1 + campaign.name = "Movie is incredible!!" + campaign + end + def build_ams_movies(count) count.times.map do |i| m = AMSMovie.new @@ -78,6 +148,7 @@ RSpec.shared_context 'ams movie class' do m.actors = ams_actors m.owner = ams_user m.movie_type = ams_movie_type + m.advertising_campaign = ams_advertising_campaign m end end diff --git a/spec/shared/contexts/group_context.rb b/spec/shared/contexts/group_context.rb index 22f2919..6c58644 100644 --- a/spec/shared/contexts/group_context.rb +++ b/spec/shared/contexts/group_context.rb @@ -26,21 +26,7 @@ RSpec.shared_context 'group class' do end end - - # Namespaced PersonSerializer - before(:context) do - # namespaced model stub - module AppName - module V1 - class PersonSerializer - include FastJsonapi::ObjectSerializer - # to test if compute_serializer_name works - end - end - end - end - - # Movie and Actor struct + # Person and Group struct before(:context) do PersonStruct = Struct.new( :id, :first_name, :last_name @@ -57,7 +43,6 @@ RSpec.shared_context 'group class' do PersonSerializer Group GroupSerializer - AppName::V1::PersonSerializer PersonStruct GroupStruct ] @@ -66,25 +51,6 @@ RSpec.shared_context 'group class' do end end - let(:group_struct) do - group = GroupStruct.new - group[:id] = 1 - group[:name] = 'Group 1' - group[:groupees] = [] - - person = PersonStruct.new - person[:id] = 1 - person[:last_name] = "Last Name 1" - person[:first_name] = "First Name 1" - - child_group = GroupStruct.new - child_group[:id] = 2 - child_group[:name] = 'Group 2' - - group.groupees = [person, child_group] - group - end - let(:group) do group = Group.new group.id = 1 diff --git a/spec/shared/contexts/movie_context.rb b/spec/shared/contexts/movie_context.rb index f26f21f..7871f8b 100644 --- a/spec/shared/contexts/movie_context.rb +++ b/spec/shared/contexts/movie_context.rb @@ -5,19 +5,20 @@ RSpec.shared_context 'movie class' do # models class Movie attr_accessor :id, - :name, + :name, :release_year, :director, - :actor_ids, - :owner_id, + :actor_ids, + :owner_id, :movie_type_id def actors - actor_ids.map do |id| + actor_ids.map.with_index do |id, i| a = Actor.new a.id = id a.name = "Test #{a.id}" a.email = "test#{a.id}@test.com" + a.agency_id = i a end end @@ -26,20 +27,107 @@ RSpec.shared_context 'movie class' do mt = MovieType.new mt.id = movie_type_id mt.name = 'Episode' + mt.movie_ids = [id] mt end + def advertising_campaign_id + 1 + end + + def advertising_campaign + ac = AdvertisingCampaign.new + ac.id = 1 + ac.movie_id = id + ac.name = "Movie #{name} is incredible!!" + ac + end + def cache_key "#{id}" end + + def url + "http://movies.com/#{id}" + end end class Actor - attr_accessor :id, :name, :email + attr_accessor :id, :name, :email, :agency_id + + def agency + Agency.new.tap do |a| + a.id = agency_id + a.name = "Test Agency #{agency_id}" + a.state_id = 1 + end + end + + def awards + award_ids.map do |i| + Award.new.tap do |a| + a.id = i + a.title = "Test Award #{i}" + a.actor_id = id + end + end + end + + def award_ids + [id * 9, id * 9 + 1] + end + + def url + "http://movies.com/actors/#{id}" + end + end + + class AdvertisingCampaign + attr_accessor :id, :name, :movie_id + end + + class Agency + attr_accessor :id, :name, :state_id + + def state + State.new.tap do |s| + s.id = state_id + s.name = "Test State #{state_id}" + s.agency_ids = [id] + end + end + end + + class Award + attr_accessor :id, :title, :actor_id + end + + class State + attr_accessor :id, :name, :agency_ids end class MovieType - attr_accessor :id, :name + attr_accessor :id, :name, :movie_ids + + def movies + movie_ids.map.with_index do |id, i| + m = Movie.new + m.id = 232 + m.name = 'test movie' + m.actor_ids = [1, 2, 3] + m.owner_id = 3 + m.movie_type_id = 1 + m + end + end + end + + class Agency + attr_accessor :id, :name, :actor_ids + end + + class Agency + attr_accessor :id, :name, :actor_ids end class Supplier @@ -67,6 +155,12 @@ RSpec.shared_context 'movie class' do has_many :actors belongs_to :owner, record_type: :user belongs_to :movie_type + has_one :advertising_campaign + end + + class MovieWithoutIdStructSerializer + include FastJsonapi::ObjectSerializer + attributes :name, :release_year end class CachingMovieSerializer @@ -95,12 +189,41 @@ RSpec.shared_context 'movie class' do include FastJsonapi::ObjectSerializer set_type :actor attributes :name, :email + belongs_to :agency + has_many :awards + belongs_to :agency + end + + class AgencySerializer + include FastJsonapi::ObjectSerializer + attributes :id, :name + belongs_to :state + has_many :actors + end + + class AwardSerializer + include FastJsonapi::ObjectSerializer + attributes :id, :title + belongs_to :actor + end + + class StateSerializer + include FastJsonapi::ObjectSerializer + attributes :id, :name + has_many :agency + end + + class AdvertisingCampaignSerializer + include FastJsonapi::ObjectSerializer + attributes :id, :name + belongs_to :movie end class MovieTypeSerializer include FastJsonapi::ObjectSerializer set_type :movie_type attributes :name + has_many :movies end class MovieSerializerWithAttributeBlock @@ -112,6 +235,21 @@ RSpec.shared_context 'movie class' do end end + class MovieSerializerWithAttributeBlock + include FastJsonapi::ObjectSerializer + set_type :movie + attributes :name, :release_year + attribute :title_with_year do |record| + "#{record.name} (#{record.release_year})" + end + end + + class AgencySerializer + include FastJsonapi::ObjectSerializer + attributes :id, :name + has_many :actors + end + class SupplierSerializer include FastJsonapi::ObjectSerializer set_type :supplier @@ -142,17 +280,20 @@ RSpec.shared_context 'movie class' do # Movie and Actor struct before(:context) do MovieStruct = Struct.new( - :id, - :name, - :release_year, - :actor_ids, - :actors, - :owner_id, - :owner, - :movie_type_id + :id, + :name, + :release_year, + :actor_ids, + :actors, + :owner_id, + :owner, + :movie_type_id, + :advertising_campaign_id ) - ActorStruct = Struct.new(:id, :name, :email) + ActorStruct = Struct.new(:id, :name, :email, :agency_id, :award_ids) + MovieWithoutIdStruct = Struct.new(:name, :release_year) + AgencyStruct = Struct.new(:id, :name, :actor_ids) end after(:context) do @@ -163,11 +304,17 @@ RSpec.shared_context 'movie class' do ActorSerializer MovieType MovieTypeSerializer - MovieSerializerWithAttributeBlock AppName::V1::MovieSerializer MovieStruct ActorStruct + MovieWithoutIdStruct HyphenMovieSerializer + MovieWithoutIdStructSerializer + Agency + AgencyStruct + AgencySerializer + AdvertisingCampaign + AdvertisingCampaignSerializer ] classes_to_remove.each do |klass_name| Object.send(:remove_const, klass_name) if Object.constants.include?(klass_name) @@ -176,10 +323,12 @@ RSpec.shared_context 'movie class' do let(:movie_struct) do + agency = AgencyStruct + actors = [] 3.times.each do |id| - actors << ActorStruct.new(id, id.to_s, id.to_s) + actors << ActorStruct.new(id, id.to_s, id.to_s, id, [id]) end m = MovieStruct.new @@ -193,6 +342,10 @@ RSpec.shared_context 'movie class' do m end + let(:movie_struct_without_id) do + MovieWithoutIdStruct.new('struct without id', 2018) + end + let(:movie) do m = Movie.new m.id = 232 @@ -203,6 +356,25 @@ RSpec.shared_context 'movie class' do m end + let(:actor) do + Actor.new.tap do |a| + a.id = 234 + a.name = 'test actor' + a.email = 'test@test.com' + a.agency_id = 432 + end + end + + let(:movie_type) do + movie + + mt = MovieType.new + mt.id = movie.movie_type_id + mt.name = 'Foreign Thriller' + mt.movie_ids = [movie.id] + mt + end + let(:supplier) do s = Supplier.new s.id = 1 diff --git a/spec/shared/examples/object_serializer_class_methods_examples.rb b/spec/shared/examples/object_serializer_class_methods_examples.rb new file mode 100644 index 0000000..c529dcb --- /dev/null +++ b/spec/shared/examples/object_serializer_class_methods_examples.rb @@ -0,0 +1,18 @@ +RSpec.shared_examples 'returning correct relationship hash' do |serializer, id_method_name, record_type| + it 'returns correct relationship hash' do + expect(relationship).to be_instance_of(Hash) + expect(relationship.keys).to all(be_instance_of(Symbol)) + expect(relationship[:serializer]).to be serializer + expect(relationship[:id_method_name]).to be id_method_name + expect(relationship[:record_type]).to be record_type + end +end + +RSpec.shared_examples 'returning key transformed hash' do |movie_type, release_year| + 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[:included][0][:type]).to eq(movie_type) + end +end diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index d7aac0c..67caaaa 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -1,3 +1,4 @@ +require 'active_record' require 'fast_jsonapi' require 'rspec-benchmark' require 'byebug' @@ -7,6 +8,7 @@ require 'jsonapi/serializable' require 'jsonapi-serializers' Dir[File.dirname(__FILE__) + '/shared/contexts/*.rb'].each {|file| require file } +Dir[File.dirname(__FILE__) + '/shared/examples/*.rb'].each {|file| require file } RSpec.configure do |config| config.include RSpec::Benchmark::Matchers