diff --git a/.gitignore b/.gitignore index cd2acb28..2bc7e6c8 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ tmp .ruby-version .ruby-gemset vendor/bundle +tags diff --git a/.simplecov b/.simplecov index 616df804..955a6060 100644 --- a/.simplecov +++ b/.simplecov @@ -19,7 +19,6 @@ ENV['FULL_BUILD'] ||= ENV['CI'] # rubocop:enable Style/DoubleNegation ## CONFIGURE SIMPLECOV -SimpleCov.pid = $$ # In case there's any forking SimpleCov.profiles.define 'app' do coverage_dir 'coverage' diff --git a/CHANGELOG.md b/CHANGELOG.md index 721f42ac..86086f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,30 @@ ## 0.10.x Breaking changes: + Features: +- [#1515](https://github.com/rails-api/active_model_serializers/pull/1515) Adds support for symbols to the + `ActiveModel::Serializer.type` method. (@groyoh) +- [#1504](https://github.com/rails-api/active_model_serializers/pull/1504) Adds the changes missing from #1454 + and add more tests for resource identifier and relationship objects. Fix association block with link + returning `data: nil`.(@groyoh) +- [#1372](https://github.com/rails-api/active_model_serializers/pull/1372) Support + cache_store.read_multi. (@LcpMarvel) +- [#1018](https://github.com/rails-api/active_model_serializers/pull/1018) Add more tests and docs for top-level links. (@leandrocp) +- [#1454](https://github.com/rails-api/active_model_serializers/pull/1454) Add support for + relationship-level links and meta attributes. (@beauby) +- [#1340](https://github.com/rails-api/active_model_serializers/pull/1340) Add support for resource-level meta. (@beauby) + Fixes: +- [#1516](https://github.com/rails-api/active_model_serializers/pull/1501) No longer return a nil href when only + adding meta to a relationship link. (@groyoh) +- [#1458](https://github.com/rails-api/active_model_serializers/pull/1458) Preserve the serializer + type when fragment caching. (@bdmac) +- [#1477](https://github.com/rails-api/active_model_serializers/pull/1477) Fix `fragment_cached?` + method to check if caching. (@bdmac) +- [#1501](https://github.com/rails-api/active_model_serializers/pull/1501) Adds tests for SerializableResource::use_adapter?,doc typos (@domitian) - [#1488](https://github.com/rails-api/active_model_serializers/pull/1488) Require ActiveSupport's string inflections (@nate00) + Misc: ### v0.10.0.rc4 (2016/01/27 11:00 +00:00) @@ -415,7 +436,7 @@ Features: '[Revert the serializers API as other alternatives are now also under discussion](https://github.com/rails/rails/commit/0a4035b12a6c59253cb60f9e3456513c6a6a9d33)'. - [Proposed Implementation to Rails 3.2 by @wycats and @josevalim (November 25, 2011)](https://github.com/rails/rails/pull/3753) - [Creation of `ActionController::Serialization`, initial serializer - support (September, 26 2011)](https://github.com/rails/rails/commit/8ff7693a8dc61f43fc4eaf72ed24d3b8699191fe0). + support (September, 26 2011)](https://github.com/rails/rails/commit/8ff7693a8dc61f43fc4eaf72ed24d3b8699191fe). - [Docs and CHANGELOG](https://github.com/rails/rails/commit/696d01f7f4a8ed787924a41cce6df836cd73c46f) - [Deprecation of ActiveModel::Serialization to ActiveModel::Serializable](https://github.com/rails/rails/blob/696d01f7f4a8ed787924a41cce6df836cd73c46f/activemodel/lib/active_model/serialization.rb) - [Creation of `ActiveModel::Serialization` from `ActiveModel::Serializer` in Rails (2009)](https://github.com/rails/rails/commit/c6bc8e662614be711f45a8d4b231d5f993b024a7#diff-d029b9768d8df0407a35804a468e3ae5) diff --git a/Gemfile b/Gemfile index 9a386356..9f9282b5 100644 --- a/Gemfile +++ b/Gemfile @@ -41,7 +41,6 @@ group :test do gem 'activerecord-jdbcsqlite3-adapter', platform: :jruby gem 'codeclimate-test-reporter', require: false - gem 'simplecov', '~> 0.10', require: false, group: :development end group :development, :test do diff --git a/README.md b/README.md index 8a48cae1..c65b2d8c 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ (Windows: [![Build status](https://ci.appveyor.com/api/projects/status/x6xdjydutm54gvyt/branch/master?svg=true)](https://ci.appveyor.com/project/joaomdmoura/active-model-serializers/branch/master)) [![Code Quality](https://codeclimate.com/github/rails-api/active_model_serializers/badges/gpa.svg)](https://codeclimate.com/github/rails-api/active_model_serializers) [![Test Coverage](https://codeclimate.com/github/rails-api/active_model_serializers/badges/coverage.svg)](https://codeclimate.com/github/rails-api/active_model_serializers/coverage) +[![Issue Stats](http://issuestats.com/github/rails-api/active_model_serializers/badge/pr)](http://issuestats.com/github/rails-api/active_model_serializers) [![Issue Stats](http://issuestats.com/github/rails-api/active_model_serializers/badge/issue)](http://issuestats.com/github/rails-api/active_model_serializers) ## Documentation diff --git a/Rakefile b/Rakefile index 4106d987..ef76e7a9 100644 --- a/Rakefile +++ b/Rakefile @@ -29,7 +29,7 @@ else Rake::Task[:rubocop].clear if Rake::Task.task_defined?(:rubocop) desc 'Execute rubocop' RuboCop::RakeTask.new(:rubocop) do |task| - task.options = ['--rails', '--display-cop-names', '--display-style-guide'] + task.options = ['--display-cop-names', '--display-style-guide'] task.fail_on_error = true end end diff --git a/active_model_serializers.gemspec b/active_model_serializers.gemspec index 2f5def5a..7febe809 100644 --- a/active_model_serializers.gemspec +++ b/active_model_serializers.gemspec @@ -51,6 +51,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'will_paginate', '~> 3.0', '>= 3.0.7' spec.add_development_dependency 'bundler', '~> 1.6' + spec.add_development_dependency 'simplecov', '~> 0.11' spec.add_development_dependency 'timecop', '~> 0.7' spec.add_development_dependency 'minitest-reporters' spec.add_development_dependency 'grape', ['>= 0.13', '< 1.0'] diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 46f1fbfd..25bb88a4 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -56,7 +56,7 @@ High-level overview: - `:each_serializer` specifies the serializer for each resource in the collection. - For a single resource, the `:serializer` option is the resource serializer. - Options are partitioned in serializer options and adapter options. Keys for adapter options are specified by - [`ADAPTER_OPTIONS`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializable_resource.rb#L4). + [`ADAPTER_OPTION_KEYS`](https://github.com/rails-api/active_model_serializers/blob/master/lib/active_model/serializable_resource.rb#L4). The remaining options are serializer options. Details: @@ -64,7 +64,7 @@ Details: 1. **ActionController::Serialization** 1. `serializable_resource = ActiveModel::SerializableResource.new(resource, options)` 1. `options` are partitioned into `adapter_opts` and everything else (`serializer_opts`). - The adapter options keys for the are defined by `ADAPTER_OPTIONS`. + The `adapter_opts` keys are defined in `ActiveModel::SerializableResource::ADAPTER_OPTION_KEYS`. 1. **ActiveModel::SerializableResource** 1. `if serializable_resource.serializer?` (there is a serializer for the resource, and an adapter is used.) - Where `serializer?` is `use_adapter? && !!(serializer)` diff --git a/docs/general/rendering.md b/docs/general/rendering.md index 1eefe652..b8349325 100644 --- a/docs/general/rendering.md +++ b/docs/general/rendering.md @@ -103,7 +103,46 @@ PR please :) #### links -PR please :) +##### How to add top-level links + +JsonApi supports a [links object](http://jsonapi.org/format/#document-links) to be specified at top-level, that you can specify in the `render`: + +```ruby + links_object = { + href: "http://example.com/api/posts", + meta: { + count: 10 + } + } + render json: @posts, links: links_object +``` + +That's the result: + +```json +{ + "data": [ + { + "type": "posts", + "id": "1", + "attributes": { + "title": "JSON API is awesome!", + "body": "You should be using JSON API", + "created": "2015-05-22T14:56:29.000Z", + "updated": "2015-05-22T14:56:28.000Z" + } + } + ], + "links": { + "href": "http://example.com/api/posts", + "meta": { + "count": 10 + } + } +} +``` + +This feature is specific to JsonApi, so you have to use the use the [JsonApi Adapter](adapters.md#jsonapi) ### serializer_opts diff --git a/docs/general/serializers.md b/docs/general/serializers.md index 4014cfe2..65ccaa1a 100644 --- a/docs/general/serializers.md +++ b/docs/general/serializers.md @@ -107,12 +107,30 @@ end #### ::type -e.g. +The `::type` method defines the JSONAPI [type](http://jsonapi.org/format/#document-resource-object-identification) that will be rendered for this serializer. +It either takes a `String` or `Symbol` as parameter. +Note: This method is useful only when using the `:json_api` adapter. + +Examples: ```ruby class UserProfileSerializer < ActiveModel::Serializer type 'profile' end +class AuthorProfileSerializer < ActiveModel::Serializer + type :profile +end +``` + +With the `:json_api` adapter, the previous serializers would be rendered as: + +``` json +{ + "data": { + "id": "1", + "type": "profile" + } +} ``` #### ::link diff --git a/docs/howto/add_pagination_links.md b/docs/howto/add_pagination_links.md index 64b903fb..b0552f4b 100644 --- a/docs/howto/add_pagination_links.md +++ b/docs/howto/add_pagination_links.md @@ -74,32 +74,30 @@ ActiveModelSerializers pagination relies on a paginated collection with the meth If you are using `JSON` adapter, pagination links will not be included automatically, but it is possible to do so using `meta` key. -In your action specify a custom serializer. -```ruby -render json: @posts, serializer: PaginatedSerializer, each_serializer: PostPreviewSerializer -``` +Add this method to your base API controller. -And then, you could do something like the following class. ```ruby -class PaginatedSerializer < ActiveModel::Serializer::CollectionSerializer - def initialize(object, options={}) - meta_key = options[:meta_key] || :meta - options[meta_key] ||= {} - options[meta_key] = { - current_page: object.current_page, - next_page: object.next_page, - prev_page: object.prev_page, - total_pages: object.total_pages, - total_count: object.total_count - } - super(object, options) - end +def pagination_dict(object) + { + current_page: object.current_page, + next_page: object.next_page, + prev_page: object.prev_page, + total_pages: object.total_pages, + total_count: object.total_count + } end ``` + +Then, use it on your render method. + +```ruby +render json: posts, meta: pagination_dict(posts) +``` + ex. ```json { - "articles": [ + "posts": [ { "id": 2, "title": "JSON API paints my bikeshed!", diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 29b76511..b3d6a1fa 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -9,6 +9,7 @@ require 'active_model/serializer/configuration' require 'active_model/serializer/fieldset' require 'active_model/serializer/lint' require 'active_model/serializer/links' +require 'active_model/serializer/meta' require 'active_model/serializer/type' # ActiveModel::Serializer is an abstract class that is @@ -20,6 +21,7 @@ module ActiveModel include Attributes include Caching include Links + include Meta include Type # Deprecated require 'active_model_serializers/adapter' @@ -70,7 +72,7 @@ module ActiveModel # @api private # Find a serializer from a class and caches the lookup. - # Preferentially retuns: + # Preferentially returns: # 1. class name appended with "Serializer" # 2. try again with superclass, if present # 3. nil diff --git a/lib/active_model/serializer/adapter/json_api/api_objects.rb b/lib/active_model/serializer/adapter/json_api/api_objects.rb new file mode 100644 index 00000000..bad3173c --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/api_objects.rb @@ -0,0 +1,13 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + extend ActiveSupport::Autoload + autoload :Relationship + autoload :ResourceIdentifier + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb b/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb new file mode 100644 index 00000000..dfaabc39 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb @@ -0,0 +1,52 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class Relationship + def initialize(parent_serializer, serializer, options = {}, links = {}, meta = nil) + @object = parent_serializer.object + @scope = parent_serializer.scope + + @options = options + @data = data_for(serializer, options) + @links = links.each_with_object({}) do |(key, value), hash| + hash[key] = ActiveModelSerializers::Adapter::JsonApi::Link.new(parent_serializer, value).as_json + end + @meta = meta.respond_to?(:call) ? parent_serializer.instance_eval(&meta) : meta + end + + def as_json + hash = {} + hash[:data] = data if options[:include_data] + links = self.links + hash[:links] = links if links.any? + meta = self.meta + hash[:meta] = meta if meta + + hash + end + + protected + + attr_reader :object, :scope, :data, :options, :links, :meta + + private + + def data_for(serializer, options) + if serializer.respond_to?(:each) + serializer.map { |s| ResourceIdentifier.new(s).as_json } + else + if options[:virtual_value] + options[:virtual_value] + elsif serializer && serializer.object + ResourceIdentifier.new(serializer).as_json + end + end + end + end + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb b/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb new file mode 100644 index 00000000..058f0603 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb @@ -0,0 +1,39 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class ResourceIdentifier + def initialize(serializer) + @id = id_for(serializer) + @type = type_for(serializer) + end + + def as_json + { id: id, type: type } + end + + protected + + attr_reader :id, :type + + private + + def type_for(serializer) + return serializer._type if serializer._type + if ActiveModelSerializers.config.jsonapi_resource_type == :singular + serializer.object.class.model_name.singular + else + serializer.object.class.model_name.plural + end + end + + def id_for(serializer) + serializer.read_attribute_for_serialization(:id).to_s + end + end + end + end + end + end +end diff --git a/lib/active_model/serializer/adapter/json_api/meta.rb b/lib/active_model/serializer/adapter/json_api/meta.rb new file mode 100644 index 00000000..8fba8986 --- /dev/null +++ b/lib/active_model/serializer/adapter/json_api/meta.rb @@ -0,0 +1,29 @@ +module ActiveModel + class Serializer + module Adapter + class JsonApi + class Meta + def initialize(serializer) + @object = serializer.object + @scope = serializer.scope + + # Use the return value of the block unless it is nil. + if serializer._meta.respond_to?(:call) + @value = instance_eval(&serializer._meta) + else + @value = serializer._meta + end + end + + def as_json + @value + end + + protected + + attr_reader :object, :scope + end + end + end + end +end diff --git a/lib/active_model/serializer/association.rb b/lib/active_model/serializer/association.rb index 1003f0a6..cbe16752 100644 --- a/lib/active_model/serializer/association.rb +++ b/lib/active_model/serializer/association.rb @@ -9,7 +9,7 @@ module ActiveModel # @example # Association.new(:comments, CommentSummarySerializer) # - Association = Struct.new(:name, :serializer, :options) do + Association = Struct.new(:name, :serializer, :options, :links, :meta) do # @return [Symbol] # def key diff --git a/lib/active_model/serializer/meta.rb b/lib/active_model/serializer/meta.rb new file mode 100644 index 00000000..5160585e --- /dev/null +++ b/lib/active_model/serializer/meta.rb @@ -0,0 +1,29 @@ +module ActiveModel + class Serializer + module Meta + extend ActiveSupport::Concern + + included do + with_options instance_writer: false, instance_reader: true do |serializer| + serializer.class_attribute :_meta # @api private + end + + extend ActiveSupport::Autoload + end + + module ClassMethods + # Set the JSON API meta attribute of a serializer. + # @example + # class AdminAuthorSerializer < ActiveModel::Serializer + # meta { stuff: 'value' } + # @example + # meta do + # { comment_count: object.comments.count } + # end + def meta(value = nil, &block) + self._meta = block || value + end + end + end + end +end diff --git a/lib/active_model/serializer/reflection.rb b/lib/active_model/serializer/reflection.rb index c0287b64..d7378e60 100644 --- a/lib/active_model/serializer/reflection.rb +++ b/lib/active_model/serializer/reflection.rb @@ -34,6 +34,43 @@ module ActiveModel # So you can inspect reflections in your Adapters. # class Reflection < Field + def initialize(*) + super + @_links = {} + @_include_data = true + end + + def link(name, value = nil, &block) + @_links[name] = block || value + :nil + end + + def meta(value = nil, &block) + @_meta = block || value + :nil + end + + def include_data(value = true) + @_include_data = value + :nil + end + + def value(serializer) + @object = serializer.object + @scope = serializer.scope + + if block + block_value = instance_eval(&block) + if block_value == :nil + serializer.read_attribute_for_serialization(name) + else + block_value + end + else + serializer.read_attribute_for_serialization(name) + end + end + # Build association. This method is used internally to # build serializer's association by its reflection. # @@ -59,6 +96,7 @@ module ActiveModel association_value = value(subject) reflection_options = options.dup serializer_class = subject.class.serializer_for(association_value, reflection_options) + reflection_options[:include_data] = @_include_data if serializer_class begin @@ -73,9 +111,13 @@ module ActiveModel reflection_options[:virtual_value] = association_value end - Association.new(name, serializer, reflection_options) + Association.new(name, serializer, reflection_options, @_links, @_meta) end + protected + + attr_accessor :object, :scope + private def serializer_options(subject, parent_serializer_options, reflection_options) diff --git a/lib/active_model/serializer/type.rb b/lib/active_model/serializer/type.rb index 563cb694..c37c9af8 100644 --- a/lib/active_model/serializer/type.rb +++ b/lib/active_model/serializer/type.rb @@ -17,7 +17,7 @@ module ActiveModel # class AdminAuthorSerializer < ActiveModel::Serializer # type 'authors' def type(type) - self._type = type + self._type = type && type.to_s end end end diff --git a/lib/active_model_serializers/adapter/attributes.rb b/lib/active_model_serializers/adapter/attributes.rb index 34fb2583..c89417e8 100644 --- a/lib/active_model_serializers/adapter/attributes.rb +++ b/lib/active_model_serializers/adapter/attributes.rb @@ -4,6 +4,7 @@ module ActiveModelSerializers def initialize(serializer, options = {}) super @include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(options[:include] || '*') + @cached_attributes = options[:cache_attributes] || {} end def serializable_hash(options = nil) @@ -23,9 +24,38 @@ module ActiveModelSerializers private def serializable_hash_for_collection(options) + cache_attributes + serializer.map { |s| Attributes.new(s, instance_options).serializable_hash(options) } end + # Read cache from cache_store + # @return [Hash] + def cache_read_multi + return {} if ActiveModelSerializers.config.cache_store.blank? + + keys = CachedSerializer.object_cache_keys(serializer, @include_tree) + + return {} if keys.blank? + + ActiveModelSerializers.config.cache_store.read_multi(*keys) + end + + # Set @cached_attributes + def cache_attributes + return if @cached_attributes.present? + + @cached_attributes = cache_read_multi + end + + # Get attributes from @cached_attributes + # @return [Hash] cached attributes + def cached_attributes(cached_serializer) + return yield unless cached_serializer.cached? + + @cached_attributes.fetch(cached_serializer.cache_key) { yield } + end + def serializable_hash_for_single_resource(options) resource = resource_object_for(options) relationships = resource_relationships(options) @@ -55,8 +85,12 @@ module ActiveModelSerializers end def resource_object_for(options) - cache_check(serializer) do - serializer.attributes(options[:fields]) + cached_serializer = CachedSerializer.new(serializer) + + cached_attributes(cached_serializer) do + cached_serializer.cache_check(self) do + serializer.attributes(options[:fields]) + end end end end diff --git a/lib/active_model_serializers/adapter/cached_serializer.rb b/lib/active_model_serializers/adapter/cached_serializer.rb index 685c5ef4..45821668 100644 --- a/lib/active_model_serializers/adapter/cached_serializer.rb +++ b/lib/active_model_serializers/adapter/cached_serializer.rb @@ -23,14 +23,16 @@ module ActiveModelSerializers end def fragment_cached? - @klass._cache_only && !@klass._cache_except || !@klass._cache_only && @klass._cache_except + @klass._cache && (@klass._cache_only && !@klass._cache_except || !@klass._cache_only && @klass._cache_except) end def cache_key + return @cache_key if defined?(@cache_key) + parts = [] parts << object_cache_key parts << @klass._cache_digest unless @klass._cache_options && @klass._cache_options[:skip_digest] - parts.join('/') + @cache_key = parts.join('/') end def object_cache_key @@ -38,6 +40,38 @@ module ActiveModelSerializers object_time_safe = object_time_safe.strftime('%Y%m%d%H%M%S%9N') if object_time_safe.respond_to?(:strftime) (@klass._cache_key) ? "#{@klass._cache_key}/#{@cached_serializer.object.id}-#{object_time_safe}" : @cached_serializer.object.cache_key end + + # find all cache_key for the collection_serializer + # @param collection_serializer + # @param include_tree + # @return [Array] all cache_key of collection_serializer + def self.object_cache_keys(serializers, include_tree) + cache_keys = [] + + serializers.each do |serializer| + cache_keys << object_cache_key(serializer) + + serializer.associations(include_tree).each do |association| + if association.serializer.respond_to?(:each) + association.serializer.each do |sub_serializer| + cache_keys << object_cache_key(sub_serializer) + end + else + cache_keys << object_cache_key(association.serializer) + end + end + end + + cache_keys.compact.uniq + end + + # @return [String, nil] the cache_key of the serializer or nil + def self.object_cache_key(serializer) + return unless serializer.present? && serializer.object.present? + + cached_serializer = new(serializer) + cached_serializer.cached? ? cached_serializer.cache_key : nil + end end end end diff --git a/lib/active_model_serializers/adapter/fragment_cache.rb b/lib/active_model_serializers/adapter/fragment_cache.rb index c7a2b059..7dbc2a87 100644 --- a/lib/active_model_serializers/adapter/fragment_cache.rb +++ b/lib/active_model_serializers/adapter/fragment_cache.rb @@ -93,6 +93,10 @@ module ActiveModelSerializers cached.constantize.cache(klass._cache_options) + # Preserve the type setting in the cached/non-cached serializer classes + cached.constantize.type(klass._type) + non_cached.constantize.type(klass._type) + cached.constantize.fragmented(serializer) non_cached.constantize.fragmented(serializer) diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb index c85b6523..841185f0 100644 --- a/lib/active_model_serializers/adapter/json_api.rb +++ b/lib/active_model_serializers/adapter/json_api.rb @@ -5,7 +5,9 @@ module ActiveModelSerializers autoload :PaginationLinks autoload :FragmentCache autoload :Link + require 'active_model/serializer/adapter/json_api/meta' autoload :Deserialization + require 'active_model/serializer/adapter/json_api/api_objects' # TODO: if we like this abstraction and other API objects to it, # then extract to its own file and require it. @@ -96,7 +98,7 @@ module ActiveModelSerializers end def process_resource(serializer, primary) - resource_identifier = resource_identifier_for(serializer) + resource_identifier = ActiveModel::Serializer::Adapter::JsonApi::ApiObjects::ResourceIdentifier.new(serializer).as_json return false unless @resource_identifiers.add?(resource_identifier) resource_object = resource_object_for(serializer) @@ -126,37 +128,13 @@ module ActiveModelSerializers process_relationships(serializer, include_tree) end - def resource_identifier_type_for(serializer) - return serializer._type if serializer._type - if ActiveModelSerializers.config.jsonapi_resource_type == :singular - serializer.object.class.model_name.singular - else - serializer.object.class.model_name.plural - end - end - - def resource_identifier_id_for(serializer) - if serializer.respond_to?(:id) - serializer.id - else - serializer.object.id - end - end - - def resource_identifier_for(serializer) - type = resource_identifier_type_for(serializer) - id = resource_identifier_id_for(serializer) - - { id: id.to_s, type: type } - end - def attributes_for(serializer, fields) serializer.attributes(fields).except(:id) end def resource_object_for(serializer) resource_object = cache_check(serializer) do - resource_object = resource_identifier_for(serializer) + resource_object = ActiveModel::Serializer::Adapter::JsonApi::ApiObjects::ResourceIdentifier.new(serializer).as_json requested_fields = fieldset && fieldset.fields_for(resource_object[:type]) attributes = attributes_for(serializer, requested_fields) @@ -164,33 +142,29 @@ module ActiveModelSerializers resource_object end - relationships = relationships_for(serializer) + requested_associations = fieldset.fields_for(resource_object[:type]) || '*' + relationships = relationships_for(serializer, requested_associations) resource_object[:relationships] = relationships if relationships.any? links = links_for(serializer) resource_object[:links] = links if links.any? + meta = meta_for(serializer) + resource_object[:meta] = meta unless meta.nil? + resource_object end - def relationship_value_for(serializer, options = {}) - if serializer.respond_to?(:each) - serializer.map { |s| resource_identifier_for(s) } - else - if options[:virtual_value] - options[:virtual_value] - elsif serializer && serializer.object - resource_identifier_for(serializer) - end - end - end - - def relationships_for(serializer) - resource_type = resource_identifier_type_for(serializer) - requested_associations = fieldset.fields_for(resource_type) || '*' + def relationships_for(serializer, requested_associations) include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(requested_associations) serializer.associations(include_tree).each_with_object({}) do |association, hash| - hash[association.key] = { data: relationship_value_for(association.serializer, association.options) } + hash[association.key] = ActiveModel::Serializer::Adapter::JsonApi::ApiObjects::Relationship.new( + serializer, + association.serializer, + association.options, + association.links, + association.meta + ).as_json end end @@ -203,6 +177,10 @@ module ActiveModelSerializers def pagination_links_for(serializer, options) JsonApi::PaginationLinks.new(serializer.object, options[:serialization_context]).serializable_hash(options) end + + def meta_for(serializer) + ActiveModel::Serializer::Adapter::JsonApi::Meta.new(serializer).as_json + end end end end diff --git a/lib/active_model_serializers/adapter/json_api/link.rb b/lib/active_model_serializers/adapter/json_api/link.rb index bb490e29..3408a98e 100644 --- a/lib/active_model_serializers/adapter/json_api/link.rb +++ b/lib/active_model_serializers/adapter/json_api/link.rb @@ -27,8 +27,9 @@ module ActiveModelSerializers def as_json return @value if @value - hash = { href: @href } - hash.merge!(meta: @meta) if @meta + hash = {} + hash[:href] = @href if @href + hash[:meta] = @meta if @meta hash end diff --git a/test/action_controller/serialization_test.rb b/test/action_controller/serialization_test.rb index d2fe3959..6c566300 100644 --- a/test/action_controller/serialization_test.rb +++ b/test/action_controller/serialization_test.rb @@ -45,6 +45,16 @@ module ActionController render json: @profiles, meta: { total: 10 } end + def render_array_using_implicit_serializer_and_links + with_adapter ActiveModelSerializers::Adapter::JsonApi do + @profiles = [ + Profile.new(name: 'Name 1', description: 'Description 1', comments: 'Comments 1') + ] + + render json: @profiles, links: { self: 'http://example.com/api/profiles/1' } + end + end + def render_object_with_cache_enabled @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') @author = Author.new(id: 1, name: 'Joao Moura.') @@ -254,6 +264,29 @@ module ActionController assert_equal expected.to_json, @response.body end + def test_render_array_using_implicit_serializer_and_links + get :render_array_using_implicit_serializer_and_links + + expected = { + data: [ + { + id: assigns(:profiles).first.id.to_s, + type: 'profiles', + attributes: { + name: 'Name 1', + description: 'Description 1' + } + } + ], + links: { + self: 'http://example.com/api/profiles/1' + } + } + + assert_equal 'application/json', @response.content_type + assert_equal expected.to_json, @response.body + end + def test_render_with_cache_enable expected = { id: 1, diff --git a/test/adapter/fragment_cache_test.rb b/test/adapter/fragment_cache_test.rb index 15c99d48..a240b56e 100644 --- a/test/adapter/fragment_cache_test.rb +++ b/test/adapter/fragment_cache_test.rb @@ -2,6 +2,14 @@ require 'test_helper' module ActiveModelSerializers module Adapter class FragmentCacheTest < ActiveSupport::TestCase + TypedRoleSerializer = Class.new(ActiveModel::Serializer) do + type 'my-roles' + cache only: [:name], skip_digest: true + attributes :id, :name, :description + + belongs_to :author + end + def setup super @spam = Spam::UnrelatedLink.new(id: 'spam-id-1') @@ -30,7 +38,11 @@ module ActiveModelSerializers } assert_equal(@spam_hash.fetch, expected_result) end + + def test_fragment_fetch_with_type_override + serialization = serializable(Role.new(name: 'Another Author'), serializer: TypedRoleSerializer, adapter: :json_api).serializable_hash + assert_equal(TypedRoleSerializer._type, serialization.fetch(:data).fetch(:type)) + end end end end - diff --git a/test/adapter/json_api/api_objects/relationship_test.rb b/test/adapter/json_api/api_objects/relationship_test.rb new file mode 100644 index 00000000..26577bc9 --- /dev/null +++ b/test/adapter/json_api/api_objects/relationship_test.rb @@ -0,0 +1,168 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class RelationshipTest < ActiveSupport::TestCase + def setup + @blog = Blog.new(id: 1) + @author = Author.new(id: 1, name: 'Steve K.', blog: @blog) + @serializer = BlogSerializer.new(@blog) + ActionController::Base.cache_store.clear + end + + def test_relationship_with_data + expected = { + data: { + id: '1', + type: 'blogs' + } + } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_with_nil_model + @serializer = BlogSerializer.new(nil) + expected = { data: nil } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_with_nil_serializer + @serializer = nil + expected = { data: nil } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_with_data_array + posts = [Post.new(id: 1), Post.new(id: 2)] + @serializer = ActiveModel::Serializer::CollectionSerializer.new(posts) + @author.posts = posts + @author.blog = nil + expected = { + data: [ + { + id: '1', + type: 'posts' + }, + { + id: '2', + type: 'posts' + } + ] + } + test_relationship(expected, options: { include_data: true }) + end + + def test_relationship_data_not_included + test_relationship({}, options: { include_data: false }) + end + + def test_relationship_simple_link + links = { self: 'a link' } + test_relationship({ links: { self: 'a link' } }, links: links) + end + + def test_relationship_many_links + links = { + self: 'a link', + related: 'another link' + } + expected = { + links: { + self: 'a link', + related: 'another link' + } + } + test_relationship(expected, links: links) + end + + def test_relationship_block_link + links = { self: proc { "#{object.id}" } } + expected = { links: { self: "#{@blog.id}" } } + test_relationship(expected, links: links) + end + + def test_relationship_block_link_with_meta + links = { + self: proc do + href "#{object.id}" + meta(id: object.id) + end + } + expected = { + links: { + self: { + href: "#{@blog.id}", + meta: { id: @blog.id } + } + } + } + test_relationship(expected, links: links) + end + + def test_relationship_simple_meta + meta = { id: '1' } + expected = { meta: meta } + test_relationship(expected, meta: meta) + end + + def test_relationship_block_meta + meta = proc do + { id: object.id } + end + expected = { + meta: { + id: @blog.id + } + } + test_relationship(expected, meta: meta) + end + + def test_relationship_with_everything + links = { + self: 'a link', + related: proc do + href "#{object.id}" + meta object.id + end + + } + meta = proc do + { id: object.id } + end + expected = { + data: { + id: '1', + type: 'blogs' + }, + links: { + self: 'a link', + related: { + href: '1', meta: 1 + } + }, + meta: { + id: @blog.id + } + } + test_relationship(expected, meta: meta, options: { include_data: true }, links: links) + end + + private + + def test_relationship(expected, params = {}) + options = params.fetch(:options, {}) + links = params.fetch(:links, {}) + meta = params[:meta] + parent_serializer = AuthorSerializer.new(@author) + relationship = Relationship.new(parent_serializer, @serializer, options, links, meta) + assert_equal(expected, relationship.as_json) + end + end + end + end + end + end +end diff --git a/test/adapter/json_api/api_objects/resource_identifier_test.rb b/test/adapter/json_api/api_objects/resource_identifier_test.rb new file mode 100644 index 00000000..a40f0707 --- /dev/null +++ b/test/adapter/json_api/api_objects/resource_identifier_test.rb @@ -0,0 +1,88 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + module ApiObjects + class ResourceIdentifierTest < ActiveSupport::TestCase + class WithDefinedTypeSerializer < Serializer + type 'with_defined_type' + end + + class WithDefinedIdSerializer < Serializer + def id + 'special_id' + end + end + + class FragmentedSerializer < Serializer; end + + def setup + @model = Author.new(id: 1, name: 'Steve K.') + ActionController::Base.cache_store.clear + end + + def test_defined_type + test_type(WithDefinedTypeSerializer, 'with_defined_type') + end + + def test_singular_type + test_type_inflection(AuthorSerializer, 'author', :singular) + end + + def test_plural_type + test_type_inflection(AuthorSerializer, 'authors', :plural) + end + + def test_id_defined_on_object + test_id(AuthorSerializer, @model.id.to_s) + end + + def test_id_defined_on_serializer + test_id(WithDefinedIdSerializer, 'special_id') + end + + def test_id_defined_on_fragmented + FragmentedSerializer.fragmented(WithDefinedIdSerializer.new(@author)) + test_id(FragmentedSerializer, 'special_id') + end + + private + + def test_type_inflection(serializer_class, expected_type, inflection) + original_inflection = ActiveModelSerializers.config.jsonapi_resource_type + ActiveModelSerializers.config.jsonapi_resource_type = inflection + test_type(serializer_class, expected_type) + ActiveModelSerializers.config.jsonapi_resource_type = original_inflection + end + + def test_type(serializer_class, expected_type) + serializer = serializer_class.new(@model) + resource_identifier = ResourceIdentifier.new(serializer) + expected = { + id: @model.id.to_s, + type: expected_type + } + + assert_equal(expected, resource_identifier.as_json) + end + + def test_id(serializer_class, id) + serializer = serializer_class.new(@model) + resource_identifier = ResourceIdentifier.new(serializer) + inflection = ActiveModelSerializers.config.jsonapi_resource_type + type = @model.class.model_name.send(inflection) + expected = { + id: id, + type: type + } + + assert_equal(expected, resource_identifier.as_json) + end + end + end + end + end + end +end diff --git a/test/adapter/json_api/links_test.rb b/test/adapter/json_api/links_test.rb index 4e97caa9..09c499ed 100644 --- a/test/adapter/json_api/links_test.rb +++ b/test/adapter/json_api/links_test.rb @@ -20,7 +20,7 @@ module ActiveModelSerializers def setup @post = Post.new(id: 1337, comments: [], author: nil) - @author = LinkAuthor.new(id: 1337) + @author = LinkAuthor.new(id: 1337, posts: [@post]) end def test_toplevel_links @@ -46,6 +46,24 @@ module ActiveModelSerializers assert_equal(expected, hash[:links]) end + def test_nil_toplevel_links + hash = ActiveModel::SerializableResource.new( + @post, + adapter: :json_api, + links: nil + ).serializable_hash + refute hash.key?(:links), 'No links key to be output' + end + + def test_nil_toplevel_links_json_adapter + hash = ActiveModel::SerializableResource.new( + @post, + adapter: :json, + links: nil + ).serializable_hash + refute hash.key?(:links), 'No links key to be output' + end + def test_resource_links hash = serializable(@author, adapter: :json_api).serializable_hash expected = { diff --git a/test/adapter/json_api/relationship_test.rb b/test/adapter/json_api/relationship_test.rb new file mode 100644 index 00000000..b612a980 --- /dev/null +++ b/test/adapter/json_api/relationship_test.rb @@ -0,0 +1,192 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + class RelationshipTest < ActiveSupport::TestCase + RelationshipAuthor = Class.new(::Model) + class RelationshipAuthorSerializer < ActiveModel::Serializer + has_one :bio do + link :self, '//example.com/link_author/relationships/bio' + end + + has_one :profile do + link :related do + "//example.com/profiles/#{object.profile.id}" + end + end + + has_many :locations do + link :related do + ids = object.locations.map(&:id).join(',') + href "//example.com/locations/#{ids}" + end + end + + has_many :posts do + link :related do + ids = object.posts.map(&:id).join(',') + href "//example.com/posts/#{ids}" + meta ids: ids + end + end + + has_many :comments do + link :self do + meta ids: [1] + end + end + + has_many :roles do + meta count: object.posts.count + end + + has_one :blog do + link :self, '//example.com/link_author/relationships/blog' + include_data false + end + + belongs_to :reviewer do + meta name: 'Dan Brown' + include_data true + end + + has_many :likes do + link :related do + ids = object.likes.map(&:id).join(',') + href "//example.com/likes/#{ids}" + meta ids: ids + end + meta liked: object.likes.any? + end + end + + def setup + @post = Post.new(id: 1337, comments: [], author: nil) + @blog = Blog.new(id: 1337, name: 'extra') + @bio = Bio.new(id: 1337) + @like = Like.new(id: 1337) + @role = Role.new(id: 1337) + @profile = Profile.new(id: 1337) + @location = Location.new(id: 1337) + @reviewer = Author.new(id: 1337) + @comment = Comment.new(id: 1337) + @author = RelationshipAuthor.new( + id: 1337, + posts: [@post], + blog: @blog, + reviewer: @reviewer, + bio: @bio, + likes: [@like], + roles: [@role], + locations: [@location], + profile: @profile, + comments: [@comment] + ) + end + + def test_relationship_simple_link + expected = { + data: { + id: '1337', + type: 'bios' + }, + links: { + self: '//example.com/link_author/relationships/bio' + } + } + assert_relationship(:bio, expected) + end + + def test_relationship_block_link + expected = { + data: { id: '1337', type: 'profiles' }, + links: { related: '//example.com/profiles/1337' } + } + assert_relationship(:profile, expected) + end + + def test_relationship_block_link_href + expected = { + data: [{ id: '1337', type: 'locations' }], + links: { + related: { href: '//example.com/locations/1337' } + } + } + assert_relationship(:locations, expected) + end + + def test_relationship_block_link_href_and_meta + expected = { + data: [{ id: '1337', type: 'posts' }], + links: { + related: { + href: '//example.com/posts/1337', + meta: { ids: '1337' } + } + } + } + assert_relationship(:posts, expected) + end + + def test_relationship_block_link_meta + expected = { + data: [{ id: '1337', type: 'comments' }], + links: { + self: { + meta: { ids: [1] } + } + } + } + assert_relationship(:comments, expected) + end + + def test_relationship_meta + expected = { + data: [{ id: '1337', type: 'roles' }], + meta: { count: 1 } + } + assert_relationship(:roles, expected) + end + + def test_relationship_not_including_data + expected = { + links: { self: '//example.com/link_author/relationships/blog' } + } + assert_relationship(:blog, expected) + end + + def test_relationship_including_data_explicit + expected = { + data: { id: '1337', type: 'authors' }, + meta: { name: 'Dan Brown' } + } + assert_relationship(:reviewer, expected) + end + + def test_relationship_with_everything + expected = { + data: [{ id: '1337', type: 'likes' }], + links: { + related: { + href: '//example.com/likes/1337', + meta: { ids: '1337' } + } + }, + meta: { liked: true } + } + assert_relationship(:likes, expected) + end + + private + + def assert_relationship(relationship_name, expected) + hash = serializable(@author, adapter: :json_api).serializable_hash + assert_equal(expected, hash[:data][:relationships][relationship_name]) + end + end + end + end + end +end diff --git a/test/adapter/json_api/resource_meta_test.rb b/test/adapter/json_api/resource_meta_test.rb new file mode 100644 index 00000000..7eec4365 --- /dev/null +++ b/test/adapter/json_api/resource_meta_test.rb @@ -0,0 +1,68 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + class ResourceMetaTest < Minitest::Test + class MetaHashPostSerializer < ActiveModel::Serializer + attributes :id + meta stuff: 'value' + end + + class MetaBlockPostSerializer < ActiveModel::Serializer + attributes :id + meta do + { comments_count: object.comments.count } + end + end + + def setup + @post = Post.new(id: 1337, comments: [], author: nil) + end + + def test_meta_hash_object_resource + hash = ActiveModel::SerializableResource.new( + @post, + serializer: MetaHashPostSerializer, + adapter: :json_api + ).serializable_hash + expected = { + stuff: 'value' + } + assert_equal(expected, hash[:data][:meta]) + end + + def test_meta_block_object_resource + hash = ActiveModel::SerializableResource.new( + @post, + serializer: MetaBlockPostSerializer, + adapter: :json_api + ).serializable_hash + expected = { + comments_count: @post.comments.count + } + assert_equal(expected, hash[:data][:meta]) + end + + def test_meta_object_resource_in_array + post2 = Post.new(id: 1339, comments: [Comment.new]) + posts = [@post, post2] + hash = ActiveModel::SerializableResource.new( + posts, + each_serializer: MetaBlockPostSerializer, + adapter: :json_api + ).serializable_hash + expected = { + :data => [ + { :id => '1337', :type => 'posts', :meta => { :comments_count => 0 } }, + { :id => '1339', :type => 'posts', :meta => { :comments_count => 1 } } + ] + } + assert_equal(expected, hash) + end + end + end + end + end +end diff --git a/test/adapter/json_api/resource_type_config_test.rb b/test/adapter/json_api/resource_type_config_test.rb deleted file mode 100644 index 571552d9..00000000 --- a/test/adapter/json_api/resource_type_config_test.rb +++ /dev/null @@ -1,69 +0,0 @@ -require 'test_helper' - -module ActiveModelSerializers - module Adapter - class JsonApi - class ResourceTypeConfigTest < ActiveSupport::TestCase - class ProfileTypeSerializer < ActiveModel::Serializer - attributes :name - type 'profile' - end - - def setup - @author = Author.new(id: 1, name: 'Steve K.') - @author.bio = nil - @author.roles = [] - @blog = Blog.new(id: 23, name: 'AMS Blog') - @post = Post.new(id: 42, title: 'New Post', body: 'Body') - @anonymous_post = Post.new(id: 43, title: 'Hello!!', body: 'Hello, world!!') - @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') - @post.comments = [@comment] - @post.blog = @blog - @anonymous_post.comments = [] - @anonymous_post.blog = nil - @comment.post = @post - @comment.author = nil - @post.author = @author - @anonymous_post.author = nil - @blog = Blog.new(id: 1, name: 'My Blog!!') - @blog.writer = @author - @blog.articles = [@post, @anonymous_post] - @author.posts = [] - end - - def with_jsonapi_resource_type type - old_type = ActiveModelSerializers.config.jsonapi_resource_type - ActiveModelSerializers.config.jsonapi_resource_type = type - yield - ensure - ActiveModelSerializers.config.jsonapi_resource_type = old_type - end - - def test_config_plural - with_jsonapi_resource_type :plural do - hash = serializable(@comment, adapter: :json_api).serializable_hash - assert_equal('comments', hash[:data][:type]) - end - end - - def test_config_singular - with_jsonapi_resource_type :singular do - hash = serializable(@comment, adapter: :json_api).serializable_hash - assert_equal('comment', hash[:data][:type]) - end - end - - def test_explicit_type_value - hash = serializable(@author, serializer: ProfileTypeSerializer, adapter: :json_api).serializable_hash - assert_equal('profile', hash.fetch(:data).fetch(:type)) - end - - private - - def serializable(resource, options = {}) - ActiveModel::SerializableResource.new(resource, options) - end - end - end - end -end diff --git a/test/adapter/json_api/type_test.rb b/test/adapter/json_api/type_test.rb new file mode 100644 index 00000000..d034957e --- /dev/null +++ b/test/adapter/json_api/type_test.rb @@ -0,0 +1,61 @@ +require 'test_helper' + +module ActiveModel + class Serializer + module Adapter + class JsonApi + class TypeTest < ActiveSupport::TestCase + class StringTypeSerializer < ActiveModel::Serializer + attribute :name + type 'profile' + end + + class SymbolTypeSerializer < ActiveModel::Serializer + attribute :name + type :profile + end + + setup do + @author = Author.new(id: 1, name: 'Steve K.') + end + + def test_config_plural + with_jsonapi_resource_type :plural do + assert_type(@author, 'authors') + end + end + + def test_config_singular + with_jsonapi_resource_type :singular do + assert_type(@author, 'author') + end + end + + def test_explicit_string_type_value + assert_type(@author, 'profile', serializer: StringTypeSerializer) + end + + def test_explicit_symbol_type_value + assert_type(@author, 'profile', serializer: SymbolTypeSerializer) + end + + private + + def assert_type(resource, expected_type, opts = {}) + opts = opts.reverse_merge(adapter: :json_api) + hash = serializable(resource, opts).serializable_hash + assert_equal(expected_type, hash.fetch(:data).fetch(:type)) + end + + def with_jsonapi_resource_type inflection + old_inflection = ActiveModelSerializers.config.jsonapi_resource_type + ActiveModelSerializers.config.jsonapi_resource_type = inflection + yield + ensure + ActiveModelSerializers.config.jsonapi_resource_type = old_inflection + end + end + end + end + end +end diff --git a/test/serializable_resource_test.rb b/test/serializable_resource_test.rb index d32e9fcf..69879504 100644 --- a/test/serializable_resource_test.rb +++ b/test/serializable_resource_test.rb @@ -23,5 +23,13 @@ module ActiveModel options = nil assert_equal @adapter.as_json(options), @serializable_resource.as_json(options) end + + def test_use_adapter_with_adapter_option + assert ActiveModel::SerializableResource.new(@resource, { adapter: 'json' }).use_adapter? + end + + def test_use_adapter_with_adapter_option_as_false + refute ActiveModel::SerializableResource.new(@resource, { adapter: false }).use_adapter? + end end end diff --git a/test/serializers/associations_test.rb b/test/serializers/associations_test.rb index aa0cae08..f62da8b8 100644 --- a/test/serializers/associations_test.rb +++ b/test/serializers/associations_test.rb @@ -32,13 +32,13 @@ module ActiveModel case key when :posts - assert_equal({}, options) + assert_equal({ include_data: true }, options) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) when :bio - assert_equal({}, options) + assert_equal({ include_data: true }, options) assert_nil serializer when :roles - assert_equal({}, options) + assert_equal({ include_data: true }, options) assert_kind_of(ActiveModelSerializers.config.collection_serializer, serializer) else flunk "Unknown association: #{key}" @@ -80,7 +80,7 @@ module ActiveModel flunk "Unknown association: #{key}" end - assert_equal({}, association.options) + assert_equal({ include_data: true }, association.options) end end diff --git a/test/serializers/cache_test.rb b/test/serializers/cache_test.rb index fea7f8f1..4167a77f 100644 --- a/test/serializers/cache_test.rb +++ b/test/serializers/cache_test.rb @@ -1,209 +1,238 @@ require 'test_helper' require 'tmpdir' require 'tempfile' -module ActiveModel - class Serializer - class CacheTest < ActiveSupport::TestCase - include ActiveSupport::Testing::Stream +module ActiveModelSerializers + class CacheTest < ActiveSupport::TestCase + include ActiveSupport::Testing::Stream - def setup - ActionController::Base.cache_store.clear - @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') - @post = Post.new(title: 'New Post', body: 'Body') - @bio = Bio.new(id: 1, content: 'AMS Contributor') - @author = Author.new(name: 'Joao M. D. Moura') - @blog = Blog.new(id: 999, name: 'Custom blog', writer: @author, articles: []) - @role = Role.new(name: 'Great Author') - @location = Location.new(lat: '-23.550520', lng: '-46.633309') - @place = Place.new(name: 'Amazing Place') - @author.posts = [@post] - @author.roles = [@role] - @role.author = @author - @author.bio = @bio - @bio.author = @author - @post.comments = [@comment] - @post.author = @author - @comment.post = @post - @comment.author = @author - @post.blog = @blog - @location.place = @place + def setup + ActionController::Base.cache_store.clear + @comment = Comment.new(id: 1, body: 'ZOMG A COMMENT') + @post = Post.new(title: 'New Post', body: 'Body') + @bio = Bio.new(id: 1, content: 'AMS Contributor') + @author = Author.new(name: 'Joao M. D. Moura') + @blog = Blog.new(id: 999, name: 'Custom blog', writer: @author, articles: []) + @role = Role.new(name: 'Great Author') + @location = Location.new(lat: '-23.550520', lng: '-46.633309') + @place = Place.new(name: 'Amazing Place') + @author.posts = [@post] + @author.roles = [@role] + @role.author = @author + @author.bio = @bio + @bio.author = @author + @post.comments = [@comment] + @post.author = @author + @comment.post = @post + @comment.author = @author + @post.blog = @blog + @location.place = @place - @location_serializer = LocationSerializer.new(@location) - @bio_serializer = BioSerializer.new(@bio) - @role_serializer = RoleSerializer.new(@role) - @post_serializer = PostSerializer.new(@post) - @author_serializer = AuthorSerializer.new(@author) - @comment_serializer = CommentSerializer.new(@comment) - @blog_serializer = BlogSerializer.new(@blog) + @location_serializer = LocationSerializer.new(@location) + @bio_serializer = BioSerializer.new(@bio) + @role_serializer = RoleSerializer.new(@role) + @post_serializer = PostSerializer.new(@post) + @author_serializer = AuthorSerializer.new(@author) + @comment_serializer = CommentSerializer.new(@comment) + @blog_serializer = BlogSerializer.new(@blog) + end + + def test_inherited_cache_configuration + inherited_serializer = Class.new(PostSerializer) + + assert_equal PostSerializer._cache_key, inherited_serializer._cache_key + assert_equal PostSerializer._cache_options, inherited_serializer._cache_options + end + + def test_override_cache_configuration + inherited_serializer = Class.new(PostSerializer) do + cache key: 'new-key' end - def test_inherited_cache_configuration - inherited_serializer = Class.new(PostSerializer) + assert_equal PostSerializer._cache_key, 'post' + assert_equal inherited_serializer._cache_key, 'new-key' + end - assert_equal PostSerializer._cache_key, inherited_serializer._cache_key - assert_equal PostSerializer._cache_options, inherited_serializer._cache_options + def test_cache_definition + assert_equal(ActionController::Base.cache_store, @post_serializer.class._cache) + assert_equal(ActionController::Base.cache_store, @author_serializer.class._cache) + assert_equal(ActionController::Base.cache_store, @comment_serializer.class._cache) + end + + def test_cache_key_definition + assert_equal('post', @post_serializer.class._cache_key) + assert_equal('writer', @author_serializer.class._cache_key) + assert_equal(nil, @comment_serializer.class._cache_key) + end + + def test_cache_key_interpolation_with_updated_at + render_object_with_cache(@author) + assert_equal(nil, ActionController::Base.cache_store.fetch(@author.cache_key)) + assert_equal(@author_serializer.attributes.to_json, ActionController::Base.cache_store.fetch("#{@author_serializer.class._cache_key}/#{@author_serializer.object.id}-#{@author_serializer.object.updated_at.strftime("%Y%m%d%H%M%S%9N")}").to_json) + end + + def test_default_cache_key_fallback + render_object_with_cache(@comment) + assert_equal(@comment_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(@comment.cache_key).to_json) + end + + def test_cache_options_definition + assert_equal({ expires_in: 0.1, skip_digest: true }, @post_serializer.class._cache_options) + assert_equal(nil, @blog_serializer.class._cache_options) + assert_equal({ expires_in: 1.day, skip_digest: true }, @comment_serializer.class._cache_options) + end + + def test_fragment_cache_definition + assert_equal([:name], @role_serializer.class._cache_only) + assert_equal([:content], @bio_serializer.class._cache_except) + end + + def test_associations_separately_cache + ActionController::Base.cache_store.clear + assert_equal(nil, ActionController::Base.cache_store.fetch(@post.cache_key)) + assert_equal(nil, ActionController::Base.cache_store.fetch(@comment.cache_key)) + + Timecop.freeze(Time.now) do + render_object_with_cache(@post) + + assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key)) + assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(@comment.cache_key)) end + end - def test_override_cache_configuration - inherited_serializer = Class.new(PostSerializer) do - cache key: 'new-key' - end + def test_associations_cache_when_updated + # Clean the Cache + ActionController::Base.cache_store.clear - assert_equal PostSerializer._cache_key, 'post' - assert_equal inherited_serializer._cache_key, 'new-key' + Timecop.freeze(Time.now) do + # Generate a new Cache of Post object and each objects related to it. + render_object_with_cache(@post) + + # Check if it cached the objects separately + assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key)) + assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(@comment.cache_key)) + + # Simulating update on comments relationship with Post + new_comment = Comment.new(id: 2, body: 'ZOMG A NEW COMMENT') + new_comment_serializer = CommentSerializer.new(new_comment) + @post.comments = [new_comment] + + # Ask for the serialized object + render_object_with_cache(@post) + + # Check if the the new comment was cached + assert_equal(new_comment_serializer.attributes, ActionController::Base.cache_store.fetch(new_comment.cache_key)) + assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key)) end + end - def test_cache_definition - assert_equal(ActionController::Base.cache_store, @post_serializer.class._cache) - assert_equal(ActionController::Base.cache_store, @author_serializer.class._cache) - assert_equal(ActionController::Base.cache_store, @comment_serializer.class._cache) - end + def test_fragment_fetch_with_virtual_associations + expected_result = { + id: @location.id, + lat: @location.lat, + lng: @location.lng, + place: 'Nowhere' + } - def test_cache_key_definition - assert_equal('post', @post_serializer.class._cache_key) - assert_equal('writer', @author_serializer.class._cache_key) - assert_equal(nil, @comment_serializer.class._cache_key) - end + hash = render_object_with_cache(@location) - def test_cache_key_interpolation_with_updated_at - render_object_with_cache(@author) - assert_equal(nil, ActionController::Base.cache_store.fetch(@author.cache_key)) - assert_equal(@author_serializer.attributes.to_json, ActionController::Base.cache_store.fetch("#{@author_serializer.class._cache_key}/#{@author_serializer.object.id}-#{@author_serializer.object.updated_at.strftime("%Y%m%d%H%M%S%9N")}").to_json) - end + assert_equal(hash, expected_result) + assert_equal({ place: 'Nowhere' }, ActionController::Base.cache_store.fetch(@location.cache_key)) + end - def test_default_cache_key_fallback + def test_uses_file_digest_in_cache_key + render_object_with_cache(@blog) + assert_equal(@blog_serializer.attributes, ActionController::Base.cache_store.fetch(@blog.cache_key_with_digest)) + end + + def test_cache_digest_definition + assert_equal(::Model::FILE_DIGEST, @post_serializer.class._cache_digest) + end + + def test_object_cache_keys + serializer = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment]) + include_tree = ActiveModel::Serializer::IncludeTree.from_include_args('*') + + actual = Adapter::CachedSerializer.object_cache_keys(serializer, include_tree) + + assert_equal actual.size, 3 + assert actual.any? { |key| key == 'comment/1' } + assert actual.any? { |key| key =~ %r{post/post-\d+} } + assert actual.any? { |key| key =~ %r{writer/author-\d+} } + end + + def test_cached_attributes + serializer = ActiveModel::Serializer::CollectionSerializer.new([@comment, @comment]) + + Timecop.freeze(Time.now) do render_object_with_cache(@comment) - assert_equal(@comment_serializer.attributes.to_json, ActionController::Base.cache_store.fetch(@comment.cache_key).to_json) + + attributes = Adapter::Attributes.new(serializer) + attributes.send(:cache_attributes) + cached_attributes = attributes.instance_variable_get(:@cached_attributes) + + assert_equal cached_attributes[@comment.cache_key], Comment.new(id: 1, body: 'ZOMG A COMMENT').attributes + assert_equal cached_attributes[@comment.post.cache_key], Post.new(id: 'post', title: 'New Post', body: 'Body').attributes + + writer = @comment.post.blog.writer + writer_cache_key = "writer/#{writer.id}-#{writer.updated_at.strftime("%Y%m%d%H%M%S%9N")}" + + assert_equal cached_attributes[writer_cache_key], Author.new(id: 'author', name: 'Joao M. D. Moura').attributes end + end - def test_cache_options_definition - assert_equal({ expires_in: 0.1, skip_digest: true }, @post_serializer.class._cache_options) - assert_equal(nil, @blog_serializer.class._cache_options) - assert_equal({ expires_in: 1.day, skip_digest: true }, @comment_serializer.class._cache_options) - end + def test_serializer_file_path_on_nix + path = '/Users/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb' + caller_line = "#{path}:1:in `'" + assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path + end - def test_fragment_cache_definition - assert_equal([:name], @role_serializer.class._cache_only) - assert_equal([:content], @bio_serializer.class._cache_except) - end + def test_serializer_file_path_on_windows + path = 'c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb' + caller_line = "#{path}:1:in `'" + assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path + end - def test_associations_separately_cache - ActionController::Base.cache_store.clear - assert_equal(nil, ActionController::Base.cache_store.fetch(@post.cache_key)) - assert_equal(nil, ActionController::Base.cache_store.fetch(@comment.cache_key)) + def test_serializer_file_path_with_space + path = '/Users/git/ember js/ember-crm-backend/app/serializers/lead_serializer.rb' + caller_line = "#{path}:1:in `'" + assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path + end - Timecop.freeze(Time.now) do - render_object_with_cache(@post) + def test_serializer_file_path_with_submatch + # The submatch in the path ensures we're using a correctly greedy regexp. + path = '/Users/git/ember js/ember:123:in x/app/serializers/lead_serializer.rb' + caller_line = "#{path}:1:in `'" + assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path + end - assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key)) - assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(@comment.cache_key)) - end - end + def test_digest_caller_file + contents = "puts 'AMS rocks'!" + dir = Dir.mktmpdir('space char') + file = Tempfile.new('some_ruby.rb', dir) + file.write(contents) + path = file.path + caller_line = "#{path}:1:in `'" + file.close + assert_equal ActiveModel::Serializer.digest_caller_file(caller_line), Digest::MD5.hexdigest(contents) + ensure + file.unlink + FileUtils.remove_entry dir + end - def test_associations_cache_when_updated - # Clean the Cache - ActionController::Base.cache_store.clear + def test_warn_on_serializer_not_defined_in_file + called = false + serializer = Class.new(ActiveModel::Serializer) + assert_match(/_cache_digest/, (capture(:stderr) do + serializer.digest_caller_file('') + called = true + end)) + assert called + end - Timecop.freeze(Time.now) do - # Generate a new Cache of Post object and each objects related to it. - render_object_with_cache(@post) + private - # Check if it cached the objects separately - assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key)) - assert_equal(@comment_serializer.attributes, ActionController::Base.cache_store.fetch(@comment.cache_key)) - - # Simulating update on comments relationship with Post - new_comment = Comment.new(id: 2, body: 'ZOMG A NEW COMMENT') - new_comment_serializer = CommentSerializer.new(new_comment) - @post.comments = [new_comment] - - # Ask for the serialized object - render_object_with_cache(@post) - - # Check if the the new comment was cached - assert_equal(new_comment_serializer.attributes, ActionController::Base.cache_store.fetch(new_comment.cache_key)) - assert_equal(@post_serializer.attributes, ActionController::Base.cache_store.fetch(@post.cache_key)) - end - end - - def test_fragment_fetch_with_virtual_associations - expected_result = { - id: @location.id, - lat: @location.lat, - lng: @location.lng, - place: 'Nowhere' - } - - hash = render_object_with_cache(@location) - - assert_equal(hash, expected_result) - assert_equal({ place: 'Nowhere' }, ActionController::Base.cache_store.fetch(@location.cache_key)) - end - - def test_uses_file_digest_in_cache_key - render_object_with_cache(@blog) - assert_equal(@blog_serializer.attributes, ActionController::Base.cache_store.fetch(@blog.cache_key_with_digest)) - end - - def test_cache_digest_definition - assert_equal(::Model::FILE_DIGEST, @post_serializer.class._cache_digest) - end - - def test_serializer_file_path_on_nix - path = '/Users/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb' - caller_line = "#{path}:1:in `'" - assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path - end - - def test_serializer_file_path_on_windows - path = 'c:/git/emberjs/ember-crm-backend/app/serializers/lead_serializer.rb' - caller_line = "#{path}:1:in `'" - assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path - end - - def test_serializer_file_path_with_space - path = '/Users/git/ember js/ember-crm-backend/app/serializers/lead_serializer.rb' - caller_line = "#{path}:1:in `'" - assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path - end - - def test_serializer_file_path_with_submatch - # The submatch in the path ensures we're using a correctly greedy regexp. - path = '/Users/git/ember js/ember:123:in x/app/serializers/lead_serializer.rb' - caller_line = "#{path}:1:in `'" - assert_equal caller_line[ActiveModel::Serializer::CALLER_FILE], path - end - - def test_digest_caller_file - contents = "puts 'AMS rocks'!" - dir = Dir.mktmpdir('space char') - file = Tempfile.new('some_ruby.rb', dir) - file.write(contents) - path = file.path - caller_line = "#{path}:1:in `'" - file.close - assert_equal ActiveModel::Serializer.digest_caller_file(caller_line), Digest::MD5.hexdigest(contents) - ensure - file.unlink - FileUtils.remove_entry dir - end - - def test_warn_on_serializer_not_defined_in_file - called = false - serializer = Class.new(ActiveModel::Serializer) - assert_match(/_cache_digest/, (capture(:stderr) do - serializer.digest_caller_file('') - called = true - end)) - assert called - end - - private - - def render_object_with_cache(obj) - ActiveModel::SerializableResource.new(obj).serializable_hash - end + def render_object_with_cache(obj) + ActiveModel::SerializableResource.new(obj).serializable_hash end end end - diff --git a/test/serializers/cached_serializer_test.rb b/test/serializers/cached_serializer_test.rb new file mode 100644 index 00000000..e9b3ff45 --- /dev/null +++ b/test/serializers/cached_serializer_test.rb @@ -0,0 +1,80 @@ +require 'test_helper' +module ActiveModelSerializers + module Adapter + class CachedSerializerTest < ActiveSupport::TestCase + def test_cached_false_without_cache_store + cached_serializer = build do |serializer| + serializer._cache = nil + end + refute cached_serializer.cached? + end + + def test_cached_true_with_cache_store_and_without_cache_only_and_cache_except + cached_serializer = build do |serializer| + serializer._cache = Object + end + assert cached_serializer.cached? + end + + def test_cached_false_with_cache_store_and_with_cache_only + cached_serializer = build do |serializer| + serializer._cache = Object + serializer._cache_only = [:name] + end + refute cached_serializer.cached? + end + + def test_cached_false_with_cache_store_and_with_cache_except + cached_serializer = build do |serializer| + serializer._cache = Object + serializer._cache_except = [:content] + end + refute cached_serializer.cached? + end + + def test_fragment_cached_false_without_cache_store + cached_serializer = build do |serializer| + serializer._cache = nil + serializer._cache_only = [:name] + end + refute cached_serializer.fragment_cached? + end + + def test_fragment_cached_true_with_cache_store_and_cache_only + cached_serializer = build do |serializer| + serializer._cache = Object + serializer._cache_only = [:name] + end + assert cached_serializer.fragment_cached? + end + + def test_fragment_cached_true_with_cache_store_and_cache_except + cached_serializer = build do |serializer| + serializer._cache = Object + serializer._cache_except = [:content] + end + assert cached_serializer.fragment_cached? + end + + def test_fragment_cached_false_with_cache_store_and_cache_except_and_cache_only + cached_serializer = build do |serializer| + serializer._cache = Object + serializer._cache_except = [:content] + serializer._cache_only = [:name] + end + refute cached_serializer.fragment_cached? + end + + private + + def build + serializer = Class.new(ActiveModel::Serializer) + serializer._cache_key = nil + serializer._cache_options = nil + yield serializer if block_given? + serializer_instance = serializer.new(Object) + CachedSerializer.new(serializer_instance) + end + end + end +end diff --git a/test/support/simplecov.rb b/test/support/simplecov.rb deleted file mode 100644 index 2a249972..00000000 --- a/test/support/simplecov.rb +++ /dev/null @@ -1,6 +0,0 @@ -# https://github.com/colszowka/simplecov/pull/400 -# https://github.com/ruby/ruby/blob/trunk/lib/English.rb -unless defined?(English) - # The exception object passed to +raise+. - alias $ERROR_INFO $! # rubocop:disable Style/SpecialGlobalVars -end