diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc088e2..281a9dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,23 +4,25 @@ on: [push, pull_request] jobs: ruby_rails_test_matrix: - runs-on: ubuntu-18.04 + runs-on: ubuntu-latest strategy: matrix: - ruby: [2.4, 2.6] - rails: [4, 5, 6] + ruby: [2.4, 2.7, '3.0'] + rails: ['5', '6.0', '6'] exclude: - ruby: 2.4 rails: 6 + - ruby: '3.0' + rails: 5 steps: - - uses: actions/checkout@master + - uses: actions/checkout@v2 - - name: Sets up the environment - uses: actions/setup-ruby@v1 + - uses: ruby/setup-ruby@v1 with: ruby-version: ${{ matrix.ruby }} + bundler-cache: true - name: Runs code QA and tests env: @@ -29,8 +31,7 @@ jobs: rm -rf Gemfile.lock sudo apt-get update sudo apt-get install libsqlite3-dev - gem uninstall bundler -a --force - gem install bundler -v '~> 1' echo $RAILS_VERSION | grep -q '4' && export SQLITE3_VERSION='~> 1.3.6' + echo $RAILS_VERSION | grep -q '4' && RUBOCOP_VERSION='~> 0.77' bundle - rake + bundle exec rake diff --git a/.rubocop.yml b/.rubocop.yml index b287531..6b68123 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -4,12 +4,21 @@ inherit_gem: require: rubocop-performance +AllCops: + NewCops: enable + Performance: Enabled: true Rails: Enabled: true +Rails/Pluck: + Enabled: false + +Rails/NegateInclude: + Enabled: false + Style/StringLiterals: Enabled: true EnforcedStyle: single_quotes diff --git a/Gemfile b/Gemfile index ce256d1..b77b5cb 100644 --- a/Gemfile +++ b/Gemfile @@ -2,5 +2,3 @@ source 'https://rubygems.org' # Specify your gem's dependencies in jsonapi.gemspec gemspec - -gem 'jsonapi-rspec', git: 'https://github.com/jsonapi-rb/jsonapi-rspec.git' diff --git a/README.md b/README.md index 85e7983..5e86183 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ Main goals: The available features include: - * object serialization (powered by Fast JSON API) + * object serialization (powered by JSON:API Serializer, was `fast_jsonapi`) * [error handling](https://jsonapi.org/format/#errors) (parameters, validation, generic errors) * fetching of the data (support for @@ -44,7 +44,7 @@ The available features include: ## But how? -Mainly by leveraging [Fast JSON API](https://github.com/Netflix/fast_jsonapi) +Mainly by leveraging [JSON:API Serializer](https://github.com/jsonapi-serializer/jsonapi-serializer) and [Ransack](https://github.com/activerecord-hackery/ransack). Thanks to everyone who worked on these amazing projects! @@ -100,7 +100,7 @@ The naming scheme follows the `ModuleName::ClassNameSerializer` for an instance of the `ModuleName::ClassName`. Please follow the -[Fast JSON API guide](https://github.com/Netflix/fast_jsonapi#serializer-definition) +[JSON:API Serializer guide](https://github.com/jsonapi-serializer/jsonapi-serializer#serializer-definition) on how to define a serializer. To provide a different naming scheme implement the `jsonapi_serializer_class` @@ -290,6 +290,7 @@ class MyController < ActionController::Base render jsonapi: paginated end end + end ``` @@ -306,6 +307,17 @@ use the `jsonapi_pagination_meta` method: end ``` + +If you want to change the default number of items per page or define a custom logic to handle page size, use the +`jsonapi_page_size` method: + +```ruby + def jsonapi_page_size(pagination_params) + per_page = pagination_params[:size].to_f.to_i + per_page = 30 if per_page > 30 + per_page + end +``` ### Deserialization `JSONAPI::Deserialization` provides a helper to transform a `JSONAPI` document diff --git a/Rakefile b/Rakefile index 6572d7d..631bf5e 100644 --- a/Rakefile +++ b/Rakefile @@ -24,7 +24,11 @@ RuboCop::RakeTask.new('qa:code') do |task| end desc('Run CI QA tasks') -task(qa: ['qa:docs', 'qa:code']) +if ENV['RAILS_VERSION'].to_s.include?('4') + task(qa: ['qa:docs']) +else + task(qa: ['qa:docs', 'qa:code']) +end RSpec::Core::RakeTask.new(spec: :qa) task(default: :spec) diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index dcc0295..5f1d3d5 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -16,12 +16,11 @@ Gem::Specification.new do |spec| spec.homepage = 'https://github.com/stas/jsonapi.rb' spec.license = 'MIT' - spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do - `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(spec)/}) } - end + spec.files = Dir.glob('{lib,spec}/**/*', File::FNM_DOTMATCH) + spec.files += %w(LICENSE.txt README.md) spec.require_paths = ['lib'] - spec.add_dependency 'fast_jsonapi', '~> 1.5' + spec.add_dependency 'jsonapi-serializer' spec.add_dependency 'ransack' spec.add_dependency 'rack' @@ -34,7 +33,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'jsonapi-rspec' spec.add_development_dependency 'yardstick' spec.add_development_dependency 'rubocop-rails_config' - spec.add_development_dependency 'rubocop' + spec.add_development_dependency 'rubocop', ENV['RUBOCOP_VERSION'] spec.add_development_dependency 'simplecov' spec.add_development_dependency 'rubocop-performance' end diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 8d9fead..59a3e3c 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -3,9 +3,6 @@ module JSONAPI # [ActiveModel::Errors] serializer class ActiveModelErrorSerializer < ErrorSerializer - set_id :object_id - set_type :error - attribute :status do '422' end diff --git a/lib/jsonapi/error_serializer.rb b/lib/jsonapi/error_serializer.rb index 6b5fb6c..4886a13 100644 --- a/lib/jsonapi/error_serializer.rb +++ b/lib/jsonapi/error_serializer.rb @@ -1,11 +1,10 @@ -require 'fast_jsonapi' +require 'jsonapi/serializer' module JSONAPI # A simple error serializer class ErrorSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer - set_id :object_id set_type :error # Object/Hash attribute helpers. @@ -15,6 +14,12 @@ class ErrorSerializer end end + # Overwrite the ID extraction method, to skip validations + # + # @return [NilClass] + def self.id_from_record(_record, _params) + end + # Remap the root key to `errors` # # @return [Hash] diff --git a/lib/jsonapi/pagination.rb b/lib/jsonapi/pagination.rb index 4d267cf..a98d149 100644 --- a/lib/jsonapi/pagination.rb +++ b/lib/jsonapi/pagination.rb @@ -43,6 +43,8 @@ def jsonapi_pagination(resources) original_url = request.base_url + request.path + '?' pagination.each do |page_name, number| + next if page_name == :records + original_params[:page][:number] = number links[page_name] = original_url + CGI.unescape( original_params.to_query @@ -63,7 +65,7 @@ def jsonapi_pagination_meta(resources) numbers = { current: page } if resources.respond_to?(:unscope) - total = resources.unscope(:limit, :offset, :order).count() + total = resources.unscope(:limit, :offset, :order).size else # Try to fetch the cached size first total = resources.instance_variable_get(:@original_size) @@ -82,6 +84,10 @@ def jsonapi_pagination_meta(resources) numbers[:last] = last_page end + if total.present? + numbers[:records] = total + end + numbers end @@ -89,16 +95,30 @@ def jsonapi_pagination_meta(resources) # # @return [Array] with the offset, limit and the current page number def jsonapi_pagination_params - def_per_page = self.class.const_get(:JSONAPI_PAGE_SIZE).to_i - pagination = params[:page].try(:slice, :number, :size) || {} - per_page = pagination[:size].to_f.to_i - per_page = def_per_page if per_page > def_per_page || per_page < 1 + per_page = jsonapi_page_size(pagination) num = [1, pagination[:number].to_f.to_i].max [(num - 1) * per_page, per_page, num] end + # Retrieves the default page size + # + # @param per_page_param [Hash] opts the paginations params + # @option opts [String] :number the page number requested + # @option opts [String] :size the page size requested + # + # @return [Integer] + def jsonapi_page_size(pagination_params) + per_page = pagination_params[:size].to_f.to_i + + return self.class + .const_get(:JSONAPI_PAGE_SIZE) + .to_i if per_page < 1 + + per_page + end + # Fallback to Rack's parsed query string when Rails is not available # # @return [Hash] diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 297779e..1c77824 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -42,8 +42,9 @@ def self.add_errors_renderer! many = JSONAPI::Rails.is_collection?(resource, options[:is_collection]) resource = [resource] unless many - return JSONAPI::ErrorSerializer.new(resource, options) - .serialized_json unless resource.is_a?(ActiveModel::Errors) + return JSONAPI::Rails.serializer_to_json( + JSONAPI::ErrorSerializer.new(resource, options) + ) unless resource.is_a?(ActiveModel::Errors) errors = [] model = resource.instance_variable_get('@base') @@ -54,8 +55,19 @@ def self.add_errors_renderer! model_serializer = JSONAPI::Rails.serializer_class(model, false) end - details = resource.messages - details = resource.details if resource.respond_to?(:details) + details = {} + if ::Rails::VERSION::MAJOR >= 6 && ::Rails::VERSION::MINOR >= 1 + resource.map do |error| + attr = error.attribute + details[attr] ||= [] + details[attr] << error.detail.merge(message: error.message) + end + elsif resource.respond_to?(:details) + details = resource.details + else + details = resource.messages + end + details.each do |error_key, error_hashes| error_hashes.each_with_index do |error_hash, index| # Support for errors.add(:attr, 'message') @@ -69,9 +81,11 @@ def self.add_errors_renderer! end end - JSONAPI::ActiveModelErrorSerializer.new( - errors, params: { model: model, model_serializer: model_serializer } - ).serialized_json + JSONAPI::Rails.serializer_to_json( + JSONAPI::ActiveModelErrorSerializer.new( + errors, params: { model: model, model_serializer: model_serializer } + ) + ) end end @@ -103,13 +117,15 @@ def self.add_renderer! serializer_class = JSONAPI::Rails.serializer_class(resource, many) end - serializer_class.new(resource, options).serialized_json + JSONAPI::Rails.serializer_to_json( + serializer_class.new(resource, options) + ) end end # Checks if an object is a collection # - # Stolen from [FastJsonapi::ObjectSerializer], instance method. + # Stolen from [JSONAPI::Serializer], instance method. # # @param resource [Object] to check # @param force_is_collection [NilClass] flag to overwrite @@ -129,5 +145,17 @@ def self.serializer_class(resource, is_collection) "#{klass.name}Serializer".constantize end + + # Lazily returns the serializer JSON + # + # @param serializer [Object] to evaluate + # @return [String] + def self.serializer_to_json(serializer) + if serializer.respond_to?(:serialized_json) + serializer.serialized_json + else + serializer.serializable_hash.to_json + end + end end end diff --git a/lib/jsonapi/version.rb b/lib/jsonapi/version.rb index 9eee5f9..80870e2 100644 --- a/lib/jsonapi/version.rb +++ b/lib/jsonapi/version.rb @@ -1,3 +1,3 @@ module JSONAPI - VERSION = '1.5.7' + VERSION = '1.7.0' end diff --git a/spec/dummy.rb b/spec/dummy.rb index 78547ba..c92639c 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -50,7 +50,7 @@ def compound_validation end class CustomNoteSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer set_type :note belongs_to :user @@ -58,7 +58,7 @@ class CustomNoteSerializer end class UserSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer has_many :notes, serializer: CustomNoteSerializer attributes(:last_name, :created_at, :updated_at) diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index 40f51e9..3c9b1fc 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -56,8 +56,13 @@ .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) expect(response_json['errors'][0]['source']) .to eq('pointer' => '/data/relationships/user') - expect(response_json['errors'][0]['detail']) - .to eq('User can\'t be blank') + if Rails::VERSION::MAJOR >= 6 && Rails::VERSION::MINOR >= 1 + expect(response_json['errors'][0]['detail']) + .to eq('User must exist') + else + expect(response_json['errors'][0]['detail']) + .to eq('User can\'t be blank') + end end context 'with custom validations on base' do diff --git a/spec/pagination_spec.rb b/spec/pagination_spec.rb index 879c883..ec1c3c4 100644 --- a/spec/pagination_spec.rb +++ b/spec/pagination_spec.rb @@ -17,7 +17,13 @@ it do expect(response_json['data'].size).to eq(0) expect(response_json['meta']) - .to eq('many' => true, 'pagination' => { 'current' => 1 }) + .to eq( + 'many' => true, + 'pagination' => { + 'current' => 1, + 'records' => 0 + } + ) end context 'with users' do @@ -68,7 +74,8 @@ 'first' => 1, 'prev' => 1, 'next' => 3, - 'last' => 3 + 'last' => 3, + 'records' => 3 ) end end @@ -83,7 +90,8 @@ 'first' => 1, 'prev' => 1, 'next' => 3, - 'last' => 3 + 'last' => 3, + 'records' => 3 ) expect(response_json).to have_link(:self) @@ -122,7 +130,8 @@ expect(response_json['meta']['pagination']).to eq( 'current' => 3, 'first' => 1, - 'prev' => 2 + 'prev' => 2, + 'records' => 3 ) expect(response_json).to have_link(:self) @@ -161,7 +170,8 @@ expect(response_json['meta']['pagination']).to eq( 'current' => 5, 'first' => 1, - 'prev' => 4 + 'prev' => 4, + 'records' => 3 ) end end @@ -173,7 +183,8 @@ expect(response_json['meta']['pagination']).to eq( 'current' => 5, 'first' => 1, - 'prev' => 4 + 'prev' => 4, + 'records' => 3 ) expect(response_json).to have_link(:self) @@ -209,7 +220,8 @@ expect(response_json['meta']['pagination']).to eq( 'current' => 1, 'next' => 2, - 'last' => 3 + 'last' => 3, + 'records' => 3 ) expect(response_json).not_to have_link(:prev)