From 36f432dea1f26805ed86c8e614874e1764def7a9 Mon Sep 17 00:00:00 2001 From: Emil Kampp <40206+ekampp@users.noreply.github.com> Date: Thu, 9 Jul 2020 08:17:16 +0200 Subject: [PATCH 01/53] Change to maintained fork of fasts_jsonapi --- jsonapi.rb.gemspec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index dcc0295..0a2884e 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -21,7 +21,7 @@ Gem::Specification.new do |spec| end spec.require_paths = ['lib'] - spec.add_dependency 'fast_jsonapi', '~> 1.5' + spec.add_dependency 'jsonapi-serializer', '~> 2.0' spec.add_dependency 'ransack' spec.add_dependency 'rack' From 5e864a2950c97b77fbfaa9bdcd068187ee9f0de5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Thu, 9 Jul 2020 13:59:10 +0100 Subject: [PATCH 02/53] Switch to `jsonapi/serializer`. --- Gemfile | 2 -- README.md | 6 +++--- lib/jsonapi/error_serializer.rb | 4 ++-- lib/jsonapi/rails.rb | 31 ++++++++++++++++++++++++------- spec/dummy.rb | 4 ++-- 5 files changed, 31 insertions(+), 16 deletions(-) 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..453d293 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` diff --git a/lib/jsonapi/error_serializer.rb b/lib/jsonapi/error_serializer.rb index 6b5fb6c..8252156 100644 --- a/lib/jsonapi/error_serializer.rb +++ b/lib/jsonapi/error_serializer.rb @@ -1,9 +1,9 @@ -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 diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 9aae0dc..5f8aef4 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') @@ -66,9 +67,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 @@ -100,13 +103,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 @@ -126,5 +131,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/spec/dummy.rb b/spec/dummy.rb index 9239455..6f0c434 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -40,7 +40,7 @@ class Note < ActiveRecord::Base end class CustomNoteSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer set_type :note belongs_to :user @@ -48,7 +48,7 @@ class CustomNoteSerializer end class UserSerializer - include FastJsonapi::ObjectSerializer + include JSONAPI::Serializer has_many :notes, serializer: CustomNoteSerializer attributes(:last_name, :created_at, :updated_at) From 750490aa46f9ccb4c599534d72d696c80112e398 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Thu, 9 Jul 2020 14:12:31 +0100 Subject: [PATCH 03/53] Do not run rubocop on Rails 4. --- Rakefile | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) 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) From 45ccba6f2cf9396e1b5a886dd04d8e2f0af9b8ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Thu, 9 Jul 2020 14:16:24 +0100 Subject: [PATCH 04/53] Version bump. --- lib/jsonapi/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/version.rb b/lib/jsonapi/version.rb index 9eee5f9..7134ef7 100644 --- a/lib/jsonapi/version.rb +++ b/lib/jsonapi/version.rb @@ -1,3 +1,3 @@ module JSONAPI - VERSION = '1.5.7' + VERSION = '1.6.0' end From 01db1e97956703bdd323483642672888840eb56f Mon Sep 17 00:00:00 2001 From: Finn Lawrence Date: Fri, 27 Nov 2020 00:45:12 +1300 Subject: [PATCH 05/53] Add Count of Records to Pagination (#29) * Add total records to pagination * Spec for total records --- lib/jsonapi/pagination.rb | 4 ++++ spec/pagination_spec.rb | 26 +++++++++++++++++++------- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/lib/jsonapi/pagination.rb b/lib/jsonapi/pagination.rb index 4d267cf..178dd8c 100644 --- a/lib/jsonapi/pagination.rb +++ b/lib/jsonapi/pagination.rb @@ -82,6 +82,10 @@ def jsonapi_pagination_meta(resources) numbers[:last] = last_page end + if total.present? + numbers[:records] = total + end + numbers end 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) From c3255b8bb6908deaa3851f62170e08f347bef22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Thu, 26 Nov 2020 11:51:26 +0000 Subject: [PATCH 06/53] Updated GH Actions config. --- .github/workflows/ci.yml | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc088e2..53de5fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,23 +4,23 @@ 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] + rails: [5, 6] exclude: - ruby: 2.4 rails: 6 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 +29,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 From efa76c104cc7ad9247964760112c7d0ca0e314e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Thu, 26 Nov 2020 15:23:13 +0000 Subject: [PATCH 07/53] Updated rubocop bits. --- .rubocop.yml | 9 +++++++++ jsonapi.rb.gemspec | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) 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/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index 0a2884e..ff37c21 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -34,7 +34,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 From 5b0c51cc087424e1866a5bf02fdb2ab4f655328a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Thu, 26 Nov 2020 15:24:06 +0000 Subject: [PATCH 08/53] Loosen up jsonapi-serializer deps. Allow builds sans git. --- jsonapi.rb.gemspec | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index ff37c21..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 'jsonapi-serializer', '~> 2.0' + spec.add_dependency 'jsonapi-serializer' spec.add_dependency 'ransack' spec.add_dependency 'rack' From 9dee9d8f99cd028aee67deb5ddb99a535af13c4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Thu, 26 Nov 2020 15:24:17 +0000 Subject: [PATCH 09/53] Fixed failed tests. --- lib/jsonapi/active_model_error_serializer.rb | 3 --- lib/jsonapi/error_serializer.rb | 7 ++++++- 2 files changed, 6 insertions(+), 4 deletions(-) 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 8252156..4886a13 100644 --- a/lib/jsonapi/error_serializer.rb +++ b/lib/jsonapi/error_serializer.rb @@ -5,7 +5,6 @@ module JSONAPI class ErrorSerializer 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] From 3a31499b6e36b623540f04c2dedf31e116016a2f Mon Sep 17 00:00:00 2001 From: Maximilian Haack Date: Sun, 29 Nov 2020 13:13:11 +0100 Subject: [PATCH 10/53] Use `#size` instead of `#count` for pagination `#count` will always execute an SQL COUNT query whereas `#size` will check if the records are already loaded (and then call `#length`). It only executes a count when necessary. See https://api.rubyonrails.org/classes/ActiveRecord/Relation.html#method-i-size --- lib/jsonapi/pagination.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/pagination.rb b/lib/jsonapi/pagination.rb index 178dd8c..3936202 100644 --- a/lib/jsonapi/pagination.rb +++ b/lib/jsonapi/pagination.rb @@ -63,7 +63,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) From 3a685a766e12fd5be9176af7e2a54c5c2a7f7a62 Mon Sep 17 00:00:00 2001 From: Joe Korzeniewski Date: Sat, 12 Dec 2020 05:32:06 -0500 Subject: [PATCH 11/53] Total count and configurable default per_page (#31) * Remove hard coded maximum per-page limit of 30. * Don't include total in pagination links. * Wrap default per_page in a method. * Update readme with jsonapi_page_size example. * Skip :records in pagination with next. --- README.md | 10 ++++++++++ lib/jsonapi/pagination.rb | 13 ++++++++++--- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 453d293..fe94452 100644 --- a/README.md +++ b/README.md @@ -290,6 +290,7 @@ class MyController < ActionController::Base render jsonapi: paginated end end + end ``` @@ -306,6 +307,15 @@ use the `jsonapi_pagination_meta` method: end ``` + +If you want to change the default number of items per page, use the +`jsonapi_page_size` method: + +```ruby + def jsonapi_page_size + 30 + end +``` ### Deserialization `JSONAPI::Deserialization` provides a helper to transform a `JSONAPI` document diff --git a/lib/jsonapi/pagination.rb b/lib/jsonapi/pagination.rb index 3936202..03ae22d 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 @@ -93,16 +95,21 @@ 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 if per_page < 1 num = [1, pagination[:number].to_f.to_i].max [(num - 1) * per_page, per_page, num] end + # Retrieves the default page size + # + # @return [Integer] + def jsonapi_page_size + self.class.const_get(:JSONAPI_PAGE_SIZE).to_i + end + # Fallback to Rack's parsed query string when Rails is not available # # @return [Hash] From f107416854d3e33c831e3db9945e65fad548dec6 Mon Sep 17 00:00:00 2001 From: Maximilian Haack Date: Sun, 14 Feb 2021 02:23:29 +0100 Subject: [PATCH 12/53] Add Rails 6.0 to test matrix --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53de5fe..bbccaef 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: matrix: ruby: [2.4, 2.7] - rails: [5, 6] + rails: ['5', '6.0', '6'] exclude: - ruby: 2.4 rails: 6 From 870a7f0265a970ef927c7e5dbeaad7d70804c1a7 Mon Sep 17 00:00:00 2001 From: Maximilian Haack Date: Sun, 14 Feb 2021 02:27:41 +0100 Subject: [PATCH 13/53] Handle Rails 6.1 ActiveModel::Error#detail Starting with Rails 6.1 ActiveModel::Error is an actual class: > Active Model's errors are now objects with an interface that allows > your application to more easily handle and interact with errors thrown > by models. The feature[1] includes a query interface, enables more > precise testing, and access to error details. > > [1] https://github.com/rails/rails/pull/32313 As a result of this `resource.details` looks slightly different than in previous versions and notably doesn't contain `:message` anymore, if `object.errors.add(:something, message: 'some error')` was used to specify a validation error. https://github.com/rails/rails/blob/2a7ff0a5f54979b14b19f827c99295297dda411d/activemodel/lib/active_model/error.rb#L149 From the test suite: ``` note.errors.add(:title, message: 'has typos') if note.errors.key?(:title) ``` Rails 6.0: ``` { :title=>[{:error=>:invalid, :value=>"BAD_TITLE"}, {:error=>{:message=>"has typos"}}], :quantity=>[{:error=>:less_than, :value=>109, :count=>100}] } ``` Rails 6.1: ``` { :title=>[{:error=>:invalid, :value=>"BAD_TITLE"}, {:error=>:invalid}], :quantity=>[{:error=>:less_than, :value=>100, :count=>100}] } ``` The patch addresses this change by merging the error.message into its detail. --- lib/jsonapi/rails.rb | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 5f8aef4..8d29d17 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -55,8 +55,18 @@ 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 do |error_hash| From 4f7d4b76e32bd91ff3e919fb5c8d141df9b51b7f Mon Sep 17 00:00:00 2001 From: Maximilian Haack Date: Sun, 14 Feb 2021 02:43:20 +0100 Subject: [PATCH 14/53] Expect different blank validation detail Rails 6.1 Due to the change in b019536 the validation error message for a required field changes from "$x can't be blank" to "$x must exist". --- spec/errors_spec.rb | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index a67e0ac..da3d0f5 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 'required by validations' do From e62c5c35d7f540409958da02c78d82c8c42e9fff Mon Sep 17 00:00:00 2001 From: Maximilian Haack Date: Sun, 14 Feb 2021 16:49:57 -0500 Subject: [PATCH 15/53] Add Ruby 3.0 to test matrix (#39) * Add Ruby 3.0 to test matrix * Exclude Ruby 3 + Rails 5 Rails 5 doesn't support Ruby 3 --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bbccaef..281a9dd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,13 @@ jobs: strategy: matrix: - ruby: [2.4, 2.7] + 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@v2 From 7cd36989c9e4f708071fdf95b70aa7db6c7ea0bc Mon Sep 17 00:00:00 2001 From: kalle saas Date: Tue, 16 Feb 2021 15:11:23 +0100 Subject: [PATCH 16/53] always call jsonapi_page_size method on pagination enabled requests (#40) * always call jsonapi_page_size method on pagination enabled requests pass the per_page_param to the jsonapi_page_size method * pass pagination to jsonapi_page_size and handle string-to-int conversion in jsonapi_page_size * update readme to reflect changes * fix woring --- README.md | 8 +++++--- lib/jsonapi/pagination.rb | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fe94452..5e86183 100644 --- a/README.md +++ b/README.md @@ -308,12 +308,14 @@ use the `jsonapi_pagination_meta` method: ``` -If you want to change the default number of items per page, use the +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 - 30 + 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 diff --git a/lib/jsonapi/pagination.rb b/lib/jsonapi/pagination.rb index 03ae22d..a98d149 100644 --- a/lib/jsonapi/pagination.rb +++ b/lib/jsonapi/pagination.rb @@ -96,8 +96,7 @@ def jsonapi_pagination_meta(resources) # @return [Array] with the offset, limit and the current page number def jsonapi_pagination_params pagination = params[:page].try(:slice, :number, :size) || {} - per_page = pagination[:size].to_f.to_i - per_page = jsonapi_page_size if 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] @@ -105,9 +104,19 @@ def jsonapi_pagination_params # 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 - self.class.const_get(:JSONAPI_PAGE_SIZE).to_i + 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 From a28f6d4a9f8545a8dcf4536f48f838a015df499c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Wed, 17 Feb 2021 11:32:12 +0000 Subject: [PATCH 17/53] Version bump. --- lib/jsonapi/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/version.rb b/lib/jsonapi/version.rb index 7134ef7..80870e2 100644 --- a/lib/jsonapi/version.rb +++ b/lib/jsonapi/version.rb @@ -1,3 +1,3 @@ module JSONAPI - VERSION = '1.6.0' + VERSION = '1.7.0' end From edfb67662734bfc5453b227d042d7a642b72d58c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Tue, 15 Jun 2021 12:01:18 +0100 Subject: [PATCH 18/53] Fixed the readme example bug. Closes #48 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 5e86183..8c95c08 100644 --- a/README.md +++ b/README.md @@ -314,7 +314,7 @@ If you want to change the default number of items per page or define a custom lo ```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 = 30 if per_page > 30 || per_page < 1 per_page end ``` From fefefcfd120e4cd537eb03d60f39aa0f1b4e9948 Mon Sep 17 00:00:00 2001 From: David Moles Date: Tue, 15 Mar 2022 12:03:09 -0700 Subject: [PATCH 19/53] Use base serializer to detect collections (#75) --- lib/jsonapi/rails.rb | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 8d29d17..fc70a2b 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -121,15 +121,13 @@ def self.add_renderer! # Checks if an object is a collection # - # Stolen from [JSONAPI::Serializer], instance method. + # Basically forwards it to a [JSONAPI::Serializer] as there's no public API # # @param resource [Object] to check # @param force_is_collection [NilClass] flag to overwrite # @return [TrueClass] upon success def self.is_collection?(resource, force_is_collection = nil) - return force_is_collection unless force_is_collection.nil? - - resource.respond_to?(:size) && !resource.respond_to?(:each_pair) + JSONAPI::ErrorSerializer.is_collection?(resource, force_is_collection) end # Resolves resource serializer class From b531b293c5f2b90f812f77ce6f037924eb922536 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 19:44:28 +0000 Subject: [PATCH 20/53] Make ransack a soft dependency. Closes #64 --- jsonapi.rb.gemspec | 2 +- lib/jsonapi/filtering.rb | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index 5f1d3d5..4d29476 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -21,11 +21,11 @@ Gem::Specification.new do |spec| spec.require_paths = ['lib'] spec.add_dependency 'jsonapi-serializer' - spec.add_dependency 'ransack' spec.add_dependency 'rack' spec.add_development_dependency 'bundler' spec.add_development_dependency 'rails', ENV['RAILS_VERSION'] + spec.add_development_dependency 'ransack' spec.add_development_dependency 'sqlite3', ENV['SQLITE3_VERSION'] spec.add_development_dependency 'ffaker' spec.add_development_dependency 'rspec', '~> 3.0' diff --git a/lib/jsonapi/filtering.rb b/lib/jsonapi/filtering.rb index 70d47db..2afedea 100644 --- a/lib/jsonapi/filtering.rb +++ b/lib/jsonapi/filtering.rb @@ -1,5 +1,10 @@ -require 'ransack/predicate' -require_relative 'patches' +begin + require 'active_record' + require 'ransack' + require_relative 'patches' +rescue LoadError + warn('Install `ransack` gem before using `JSONAPI::Filtering`!') +end # Filtering and sorting support module JSONAPI From d701849e6d6bed2d34370e8c328a68903f915345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 19:45:07 +0000 Subject: [PATCH 21/53] Make rubocop happy. --- lib/jsonapi/rails.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index fc70a2b..8100ece 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -47,7 +47,7 @@ def self.add_errors_renderer! ) unless resource.is_a?(ActiveModel::Errors) errors = [] - model = resource.instance_variable_get('@base') + model = resource.instance_variable_get(:@base) if respond_to?(:jsonapi_serializer_class, true) model_serializer = jsonapi_serializer_class(model, false) From ae31acffa1c4cae6cd340733ed962fedb8d27a5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 19:45:30 +0000 Subject: [PATCH 22/53] Allow running tests without code QA. --- Rakefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 631bf5e..8303d25 100644 --- a/Rakefile +++ b/Rakefile @@ -30,5 +30,5 @@ else task(qa: ['qa:docs', 'qa:code']) end -RSpec::Core::RakeTask.new(spec: :qa) -task(default: :spec) +RSpec::Core::RakeTask.new(:spec) +task(default: %w[qa spec]) From e0ab1cd97ea02b2d7e9a5047b6513d4801fd896e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 19:47:36 +0000 Subject: [PATCH 23/53] Use built-in version comparison API. --- lib/jsonapi/rails.rb | 4 ++-- spec/errors_spec.rb | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 8100ece..43fd8c5 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -56,8 +56,8 @@ def self.add_errors_renderer! end details = {} - if ::Rails::VERSION::MAJOR >= 6 && ::Rails::VERSION::MINOR >= 1 - resource.map do |error| + if ::Rails.gem_version >= Gem::Version.new('6.1') + resource.each do |error| attr = error.attribute details[attr] ||= [] details[attr] << error.detail.merge(message: error.message) diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index da3d0f5..4443acf 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -56,7 +56,7 @@ .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) expect(response_json['errors'][0]['source']) .to eq('pointer' => '/data/relationships/user') - if Rails::VERSION::MAJOR >= 6 && Rails::VERSION::MINOR >= 1 + if Rails.gem_version >= Gem::Version.new('6.1') expect(response_json['errors'][0]['detail']) .to eq('User must exist') else From 80a1e9b8d5a30b78e3937dd4e08a6fce14ea9561 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 19:48:19 +0000 Subject: [PATCH 24/53] Cleanup dependencies. --- .github/workflows/ci.yml | 13 +++++++------ jsonapi.rb.gemspec | 3 ++- spec/dummy.rb | 5 ++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 281a9dd..eef4be3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,13 +8,15 @@ jobs: strategy: matrix: - ruby: [2.4, 2.7, '3.0'] - rails: ['5', '6.0', '6'] + ruby: [2.6, '3.0', '3.1'] + rails: [5, 6, 7] exclude: - - ruby: 2.4 - rails: 6 + - ruby: '2.6' + rails: 7 - ruby: '3.0' rails: 5 + - ruby: '3.1' + rails: 5 steps: - uses: actions/checkout@v2 @@ -31,7 +33,6 @@ jobs: rm -rf Gemfile.lock sudo apt-get update sudo apt-get install libsqlite3-dev - echo $RAILS_VERSION | grep -q '4' && export SQLITE3_VERSION='~> 1.3.6' - echo $RAILS_VERSION | grep -q '4' && RUBOCOP_VERSION='~> 0.77' + echo $RAILS_VERSION | grep -q '5' && export SQLITE3_VERSION='~> 1.3.6' bundle bundle exec rake diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index 4d29476..26122fa 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -24,8 +24,9 @@ Gem::Specification.new do |spec| spec.add_dependency 'rack' spec.add_development_dependency 'bundler' - spec.add_development_dependency 'rails', ENV['RAILS_VERSION'] spec.add_development_dependency 'ransack' + spec.add_development_dependency 'railties', ENV['RAILS_VERSION'] + spec.add_development_dependency 'activerecord', ENV['RAILS_VERSION'] spec.add_development_dependency 'sqlite3', ENV['SQLITE3_VERSION'] spec.add_development_dependency 'ffaker' spec.add_development_dependency 'rspec', '~> 3.0' diff --git a/spec/dummy.rb b/spec/dummy.rb index 6f0c434..fd4354b 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -1,8 +1,7 @@ require 'securerandom' -require 'active_record' -require 'action_controller/railtie' -require 'jsonapi' +require 'rails/all' require 'ransack' +require 'jsonapi' Rails.logger = Logger.new(STDOUT) Rails.logger.level = ENV['LOG_LEVEL'] || Logger::WARN From 5f9def05a44208a3c45bdb59b3d42751f864c93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 20:08:52 +0000 Subject: [PATCH 25/53] Updated the readme. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8c95c08..6439edf 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ The available features include: [sparse fields](https://jsonapi.org/format/#fetching-sparse-fieldsets)) * [filtering](https://jsonapi.org/format/#fetching-filtering) and [sorting](https://jsonapi.org/format/#fetching-sorting) of the data - (powered by Ransack) + (powered by Ransack, soft-dependency) * [pagination](https://jsonapi.org/format/#fetching-pagination) support ## But how? @@ -231,6 +231,8 @@ to filter and sort over a collection of records. The support is pretty extended and covers also relationships and composite matchers. +Please add `ransack` to your `Gemfile` in order to benefit from this functionality! + Here's an example: ```ruby From 07aaf43a3456ac30d23f0d03494ec49bbb48c664 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 20:18:58 +0000 Subject: [PATCH 26/53] Added the sponsors. <3 --- README.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/README.md b/README.md index 6439edf..09c2e41 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,16 @@ and [Ransack](https://github.com/activerecord-hackery/ransack). Thanks to everyone who worked on these amazing projects! +## Sponsors + +I'm grateful for the following companies for supporting this project! + +

+ + +

+ + ## Installation Add this line to your application's Gemfile: From cab94c7d57d28e3dd8aedb5e1890d69981e2f636 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 21 Mar 2022 20:21:13 +0000 Subject: [PATCH 27/53] Version bump. --- lib/jsonapi/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/version.rb b/lib/jsonapi/version.rb index 80870e2..a00a501 100644 --- a/lib/jsonapi/version.rb +++ b/lib/jsonapi/version.rb @@ -1,3 +1,3 @@ module JSONAPI - VERSION = '1.7.0' + VERSION = '2.0.0' end From ef685b18a9ee6ee2f26d08f6ff4dd5e38f81f4e8 Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Tue, 28 Jun 2022 11:12:45 -0700 Subject: [PATCH 28/53] Add Dependabot for GitHub Actions --- .github/dependabot.yml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..c73a811 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,7 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + From c36320732a36d81ee6791e34145744fd8f51d171 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 7 Sep 2022 16:14:36 +0000 Subject: [PATCH 29/53] Bump actions/checkout from 2 to 3 Bumps [actions/checkout](https://github.com/actions/checkout) from 2 to 3. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index eef4be3..7255332 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: rails: 5 steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 - uses: ruby/setup-ruby@v1 with: From f9415aa45e2f3f6284d8bb52083823f9f3259b2d Mon Sep 17 00:00:00 2001 From: Yauheni Dakuka Date: Mon, 9 Aug 2021 23:19:32 +0300 Subject: [PATCH 30/53] Fix deprecation warning with Content-Type header without modification --- lib/jsonapi/rails.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 43fd8c5..47954f9 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -37,7 +37,7 @@ def self.install! # @return [NilClass] def self.add_errors_renderer! ActionController::Renderers.add(:jsonapi_errors) do |resource, options| - self.content_type ||= Mime[:jsonapi] + self.content_type = Mime[:jsonapi] if self.media_type.nil? many = JSONAPI::Rails.is_collection?(resource, options[:is_collection]) resource = [resource] unless many @@ -90,7 +90,7 @@ def self.add_errors_renderer! # @return [NilClass] def self.add_renderer! ActionController::Renderers.add(:jsonapi) do |resource, options| - self.content_type ||= Mime[:jsonapi] + self.content_type = Mime[:jsonapi] if self.media_type.nil? JSONAPI_METHODS_MAPPING.to_a[0..1].each do |opt, method_name| next unless respond_to?(method_name, true) From 5d50e67afb463311ec5d74f6c62ad7f65d7fd034 Mon Sep 17 00:00:00 2001 From: Myles Cowper-Coles Date: Thu, 21 Apr 2022 14:29:07 +0100 Subject: [PATCH 31/53] Removing empty meta and link fields if data is empty - When resources are empty, but it's an array, we should be removing empty links and meta fields if they are nil. This matches the behaviour in fast_jsonapi: https://github.com/jsonapi-serializer/jsonapi-serializer/blob/master/lib/fast_jsonapi/object_serializer.rb#L71-L72 - Currently, links will be nil if there is 1+ resource, but present when there are none --- lib/jsonapi/rails.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 47954f9..836fd90 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -100,7 +100,7 @@ def self.add_renderer! # If it's an empty collection, return it directly. many = JSONAPI::Rails.is_collection?(resource, options[:is_collection]) if many && !resource.any? - return options.slice(:meta, :links).merge(data: []).to_json + return options.slice(:meta, :links).compact.merge(data: []).to_json end JSONAPI_METHODS_MAPPING.to_a[2..-1].each do |opt, method_name| From 1f60bdf348b5d79a1b4d298f77c7bbd6a7582243 Mon Sep 17 00:00:00 2001 From: Simon Claessens Date: Mon, 14 Nov 2022 11:32:01 +0100 Subject: [PATCH 32/53] Remove annoying ransack warning #84 --- jsonapi.rb.gemspec | 4 ++++ lib/jsonapi/filtering.rb | 1 - 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index 26122fa..eec2dad 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -20,6 +20,10 @@ Gem::Specification.new do |spec| spec.files += %w(LICENSE.txt README.md) spec.require_paths = ['lib'] + spec.post_install_message = ( + 'Install manually `ransack` gem before using `JSONAPI::Filtering`!' + ) + spec.add_dependency 'jsonapi-serializer' spec.add_dependency 'rack' diff --git a/lib/jsonapi/filtering.rb b/lib/jsonapi/filtering.rb index 2afedea..48d7ef2 100644 --- a/lib/jsonapi/filtering.rb +++ b/lib/jsonapi/filtering.rb @@ -3,7 +3,6 @@ require 'ransack' require_relative 'patches' rescue LoadError - warn('Install `ransack` gem before using `JSONAPI::Filtering`!') end # Filtering and sorting support From a35c067ad8a6c5f9f9169583cc12eb4370de4a8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 5 Dec 2022 12:34:39 +0000 Subject: [PATCH 33/53] Cleanup supported CI envs. --- .github/workflows/ci.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7255332..15453c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,14 +9,10 @@ jobs: strategy: matrix: ruby: [2.6, '3.0', '3.1'] - rails: [5, 6, 7] + rails: [6, 7] exclude: - ruby: '2.6' rails: 7 - - ruby: '3.0' - rails: 5 - - ruby: '3.1' - rails: 5 steps: - uses: actions/checkout@v3 From 4ea562424da9cf53a3f9f79ef35e1b2656d90673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 5 Dec 2022 12:36:03 +0000 Subject: [PATCH 34/53] Version bump. --- lib/jsonapi/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/version.rb b/lib/jsonapi/version.rb index a00a501..e013a61 100644 --- a/lib/jsonapi/version.rb +++ b/lib/jsonapi/version.rb @@ -1,3 +1,3 @@ module JSONAPI - VERSION = '2.0.0' + VERSION = '2.0.1' end From c530c2570f93cd0ae1ae7985f43964cde436dc1a Mon Sep 17 00:00:00 2001 From: Alex <9390869+xhs345@users.noreply.github.com> Date: Mon, 12 Dec 2022 17:11:31 -0800 Subject: [PATCH 35/53] Provide example request for using sparse fields Thought it would be helpful to add an example similar to how it's done in other sections --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 09c2e41..1a7b6fd 100644 --- a/README.md +++ b/README.md @@ -233,6 +233,12 @@ class MyController < ActionController::Base end ``` +This allows you to run queries like: + +```bash +$ curl -X GET /api/resources?fields[model]=model_attr,relationship +``` + ### Filtering and sorting `JSONAPI::Filtering` uses the power of From fd05f528ff508c6e80d09e03c9e8fee8686d183b Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Mon, 26 Dec 2022 16:11:16 -0500 Subject: [PATCH 36/53] Adds Ruby 3.2 to the CI matrix. --- .github/workflows/ci.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 15453c6..75892bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,11 +8,13 @@ jobs: strategy: matrix: - ruby: [2.6, '3.0', '3.1'] + ruby: [2.6, '3.0', '3.1', 3.2] rails: [6, 7] exclude: - ruby: '2.6' rails: 7 + - ruby: 3.2 + rails: 6 steps: - uses: actions/checkout@v3 From 07a96dc7b8d709b1281d09168746e46115c2fd0e Mon Sep 17 00:00:00 2001 From: Peter Goldstein Date: Mon, 26 Dec 2022 16:13:41 -0500 Subject: [PATCH 37/53] Fix lints --- Rakefile | 2 ++ spec/pagination_spec.rb | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/Rakefile b/Rakefile index 8303d25..55a130d 100644 --- a/Rakefile +++ b/Rakefile @@ -4,6 +4,7 @@ require 'rubocop/rake_task' require 'yaml' require 'yardstick' +# rubocop:disable Rails/RakeEnvironment desc('Documentation stats and measurements') task('qa:docs') do yaml = YAML.load_file(File.expand_path('../.yardstick.yml', __FILE__)) @@ -13,6 +14,7 @@ task('qa:docs') do coverage = Yardstick.round_percentage(measure.coverage * 100) exit(1) if coverage < config.threshold end +# rubocop:enable Rails/RakeEnvironment desc('Codestyle check and linter') RuboCop::RakeTask.new('qa:code') do |task| diff --git a/spec/pagination_spec.rb b/spec/pagination_spec.rb index ec1c3c4..622c81e 100644 --- a/spec/pagination_spec.rb +++ b/spec/pagination_spec.rb @@ -58,7 +58,7 @@ page: { number: 2, size: 1 }, sort: '-created_at', as_list: as_list - }.reject { |_k, _v| _v.blank? } + }.compact_blank end context 'on an array of resources' do @@ -157,7 +157,7 @@ { page: { number: 5, size: 1 }, as_list: as_list - }.reject { |_k, _v| _v.blank? } + }.compact_blank end context 'on an array of resources' do From c3b32db6ca1626d40b2b3d11ba9a4bf458a997db Mon Sep 17 00:00:00 2001 From: Martin Meyerhoff Date: Fri, 21 Jul 2023 10:25:21 +0200 Subject: [PATCH 38/53] Fix specs with Ransack 4.0 Ransack 4 requires explicitly allowlisting ransackable attributes and associations for each model. This causes the specs to fail with the most recent Ransack version. This fixes those spec failures. --- spec/dummy.rb | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/spec/dummy.rb b/spec/dummy.rb index fd4354b..9f714e1 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -30,12 +30,28 @@ class User < ActiveRecord::Base has_many :notes + + def self.ransackable_attributes(auth_object = nil) + %w(created_at first_name id last_name updated_at) + end + + def self.ransackable_associations(auth_object = nil) + %w(notes) + end end class Note < ActiveRecord::Base validates_format_of :title, without: /BAD_TITLE/ validates_numericality_of :quantity, less_than: 100, if: :quantity? belongs_to :user, required: true + + def self.ransackable_associations(auth_object = nil) + %w(user) + end + + def self.ransackable_attributes(auth_object = nil) + %w(created_at id quantity title updated_at user_id) + end end class CustomNoteSerializer From 886218e11dbbdd651cd56f9ba0fb1f5df8ecae3e Mon Sep 17 00:00:00 2001 From: Martin Meyerhoff Date: Sun, 8 Oct 2023 22:28:41 +0200 Subject: [PATCH 39/53] Fix Rubocop offenses (#92) * Rubocop fix: Use filter_map instead of map { ... }.compact Currently, builds are failing because of Rubocop complaining about these three lines. * Drop support for Ruby 2.6 Array#filter_map is only introduced in Ruby 2.7. This ensures we're still supporting the 2.x series, but drops support for the now pretty old Ruby 2.6. --- .github/workflows/ci.yml | 4 ++-- lib/jsonapi/deserialization.rb | 2 +- lib/jsonapi/fetching.rb | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 75892bb..da8d875 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,10 +8,10 @@ jobs: strategy: matrix: - ruby: [2.6, '3.0', '3.1', 3.2] + ruby: [2.7, '3.0', '3.1', 3.2] rails: [6, 7] exclude: - - ruby: '2.6' + - ruby: '2.7' rails: 7 - ruby: 3.2 rails: 6 diff --git a/lib/jsonapi/deserialization.rb b/lib/jsonapi/deserialization.rb index 0a9d5de..dae089a 100644 --- a/lib/jsonapi/deserialization.rb +++ b/lib/jsonapi/deserialization.rb @@ -65,7 +65,7 @@ def jsonapi_deserialize(document, options = {}) rel_name = jsonapi_inflector.singularize(assoc_name) if assoc_data.is_a?(Array) - parsed["#{rel_name}_ids"] = assoc_data.map { |ri| ri['id'] }.compact + parsed["#{rel_name}_ids"] = assoc_data.filter_map { |ri| ri['id'] } next end diff --git a/lib/jsonapi/fetching.rb b/lib/jsonapi/fetching.rb index c394e8a..d5e91ec 100644 --- a/lib/jsonapi/fetching.rb +++ b/lib/jsonapi/fetching.rb @@ -17,7 +17,7 @@ def jsonapi_fields end params[:fields].each do |k, v| - extracted[k] = v.to_s.split(',').map(&:strip).compact + extracted[k] = v.to_s.split(',').filter_map(&:strip) end extracted @@ -29,7 +29,7 @@ def jsonapi_fields # # @return [Array] def jsonapi_include - params['include'].to_s.split(',').map(&:strip).compact + params['include'].to_s.split(',').filter_map(&:strip) end end end From b1c208002769beff6bddd78a114ca70148db1abd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sun, 8 Oct 2023 23:13:08 +0100 Subject: [PATCH 40/53] Bump actions/checkout from 3 to 4 (#96) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump actions/checkout from 3 to 4 Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] * Cleanup rubies/rails. --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Stas SUȘCOV --- .github/workflows/ci.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da8d875..fe0ebc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,15 +9,13 @@ jobs: strategy: matrix: ruby: [2.7, '3.0', '3.1', 3.2] - rails: [6, 7] + rails: [6, '7.0', '7.1'] exclude: - - ruby: '2.7' - rails: 7 - ruby: 3.2 rails: 6 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: ruby/setup-ruby@v1 with: From 7a105a5c4951e51f410533780a40b9a2efa79c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Sun, 8 Oct 2023 23:21:37 +0100 Subject: [PATCH 41/53] Added `code` to error attributes. Closes #95 --- lib/jsonapi/error_serializer.rb | 2 +- spec/errors_spec.rb | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi/error_serializer.rb b/lib/jsonapi/error_serializer.rb index 4886a13..cb384fa 100644 --- a/lib/jsonapi/error_serializer.rb +++ b/lib/jsonapi/error_serializer.rb @@ -8,7 +8,7 @@ class ErrorSerializer set_type :error # Object/Hash attribute helpers. - [:status, :source, :title, :detail].each do |attr_name| + [:status, :source, :title, :detail, :code].each do |attr_name| attribute attr_name do |object| object.try(attr_name) || object.try(:fetch, attr_name, nil) end diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index 4443acf..84354aa 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -32,11 +32,16 @@ it do expect(response).to have_http_status(:unprocessable_entity) expect(response_json['errors'].size).to eq(1) + + expect(response_json['errors'].first.keys) + .to contain_exactly('status', 'source', 'title', 'detail', 'code') + expect(response_json['errors'][0]['status']).to eq('422') expect(response_json['errors'][0]['title']) .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) expect(response_json['errors'][0]['source']).to eq('pointer' => '') expect(response_json['errors'][0]['detail']).to be_nil + expect(response_json['errors'][0]['code']).to be_nil end end From 796a3624c8650803ccc387b99374fe2a3a61afde Mon Sep 17 00:00:00 2001 From: Gabriel Sandoval Date: Wed, 6 Mar 2024 21:54:10 +0800 Subject: [PATCH 42/53] Fix example code on filtering and sorting section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1a7b6fd..ef6b2dc 100644 --- a/README.md +++ b/README.md @@ -282,7 +282,7 @@ grouping. To enable expressions along with filters, use the option flags: ```ruby options = { sort_with_expressions: true } jsonapi_filter(User.all, allowed_fields, options) do |filtered| - render jsonapi: result.group('id').to_a + render jsonapi: filtered.result.group('id').to_a end ``` From 6ceac38bb49185fec092f37795ae8304bcb1b4db Mon Sep 17 00:00:00 2001 From: Martin Meyerhoff Date: Wed, 31 Jan 2024 21:13:11 +0100 Subject: [PATCH 43/53] CI Matrix: Drop Rails 6, Ruby 2.7 Test Ruby 3.3 and Rails 6.1 instead --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe0ebc0..d9e837c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -8,8 +8,8 @@ jobs: strategy: matrix: - ruby: [2.7, '3.0', '3.1', 3.2] - rails: [6, '7.0', '7.1'] + ruby: ['3.0', '3.1', '3.2', '3.3'] + rails: ['6.1', '7.0', '7.1'] exclude: - ruby: 3.2 rails: 6 From d2f94b5c5ae23ae99730cd7509b8c3e060dafb5c Mon Sep 17 00:00:00 2001 From: Marcle Rodrigues Date: Sun, 23 Jun 2024 08:59:48 -0300 Subject: [PATCH 44/53] Drop activerecord dependency (#102) --- lib/jsonapi/filtering.rb | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/jsonapi/filtering.rb b/lib/jsonapi/filtering.rb index 48d7ef2..7a6ab73 100644 --- a/lib/jsonapi/filtering.rb +++ b/lib/jsonapi/filtering.rb @@ -1,6 +1,5 @@ begin - require 'active_record' - require 'ransack' + require 'ransack/predicate' require_relative 'patches' rescue LoadError end From 52b4128d1a3538cb19822d4ff90351a62fa27dc5 Mon Sep 17 00:00:00 2001 From: Martin Meyerhoff Date: Sun, 23 Jun 2024 14:10:02 +0200 Subject: [PATCH 45/53] Refactor ActiveModel::ErrorSerializer (#98) * CI Matrix: Drop Rails 6, Ruby 2.7 Test Ruby 3.3 and Rails 6.1 instead * Refactor error spec The previous specs were harder to read, and they relied on the ordering of the errors, which is unnecessary. * Add spec for non-interpolated error messages * Simplify ActiveModel::ErrorSerializer We're only running CI for Rails 6.1+ now, and we can now rely on a stable API for ActiveModel::Errors. --- lib/jsonapi/active_model_error_serializer.rb | 28 +--- lib/jsonapi/rails.rb | 28 +--- spec/dummy.rb | 6 + spec/errors_spec.rb | 165 +++++++++++-------- 4 files changed, 107 insertions(+), 120 deletions(-) diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 59a3e3c..e46521b 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -12,35 +12,15 @@ class ActiveModelErrorSerializer < ErrorSerializer end attribute :code do |object| - _, error_hash = object - code = error_hash[:error] unless error_hash[:error].is_a?(Hash) - code ||= error_hash[:message] || :invalid - # `parameterize` separator arguments are different on Rails 4 vs 5... - code.to_s.delete("''").parameterize.tr('-', '_') + object.type.to_s.delete("''").parameterize.tr('-', '_') end - attribute :detail do |object, params| - error_key, error_hash = object - errors_object = params[:model].errors - - # Rails 4 provides just the message. - if error_hash[:error].present? && error_hash[:error].is_a?(Hash) - message = errors_object.generate_message( - error_key, nil, error_hash[:error] - ) - elsif error_hash[:error].present? - message = errors_object.generate_message( - error_key, error_hash[:error], error_hash - ) - else - message = error_hash[:message] - end - - errors_object.full_message(error_key, message) + attribute :detail do |object, _params| + object.full_message end attribute :source do |object, params| - error_key, _ = object + error_key = object.attribute model_serializer = params[:model_serializer] attrs = (model_serializer.attributes_to_serialize || {}).keys rels = (model_serializer.relationships_to_serialize || {}).keys diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 836fd90..0983628 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -46,7 +46,6 @@ def self.add_errors_renderer! JSONAPI::ErrorSerializer.new(resource, options) ) unless resource.is_a?(ActiveModel::Errors) - errors = [] model = resource.instance_variable_get(:@base) if respond_to?(:jsonapi_serializer_class, true) @@ -55,31 +54,12 @@ def self.add_errors_renderer! model_serializer = JSONAPI::Rails.serializer_class(model, false) end - details = {} - if ::Rails.gem_version >= Gem::Version.new('6.1') - resource.each 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 do |error_hash| - # Rails 4 provides just the message. - error_hash = { message: error_hash } unless error_hash.is_a?(Hash) - - errors << [ error_key, error_hash ] - end - end - JSONAPI::Rails.serializer_to_json( JSONAPI::ActiveModelErrorSerializer.new( - errors, params: { model: model, model_serializer: model_serializer } + resource.errors, params: { + model: model, + model_serializer: model_serializer + } ) ) end diff --git a/spec/dummy.rb b/spec/dummy.rb index 9f714e1..8f222fd 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -41,6 +41,7 @@ def self.ransackable_associations(auth_object = nil) end class Note < ActiveRecord::Base + validate :title_cannot_contain_slurs validates_format_of :title, without: /BAD_TITLE/ validates_numericality_of :quantity, less_than: 100, if: :quantity? belongs_to :user, required: true @@ -52,6 +53,11 @@ def self.ransackable_associations(auth_object = nil) def self.ransackable_attributes(auth_object = nil) %w(created_at id quantity title updated_at user_id) end + + private + def title_cannot_contain_slurs + errors.add(:base, 'Title has slurs') if title.to_s.include?('SLURS') + end end class CustomNoteSerializer diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index 84354aa..a704a63 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -32,16 +32,15 @@ it do expect(response).to have_http_status(:unprocessable_entity) expect(response_json['errors'].size).to eq(1) - - expect(response_json['errors'].first.keys) - .to contain_exactly('status', 'source', 'title', 'detail', 'code') - - expect(response_json['errors'][0]['status']).to eq('422') - expect(response_json['errors'][0]['title']) - .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) - expect(response_json['errors'][0]['source']).to eq('pointer' => '') - expect(response_json['errors'][0]['detail']).to be_nil - expect(response_json['errors'][0]['code']).to be_nil + expect(response_json['errors']).to contain_exactly( + { + 'status' => '422', + 'source' => { 'pointer' => '' }, + 'title' => 'Unprocessable Entity', + 'detail' => nil, + 'code' => nil + } + ) end end @@ -55,19 +54,20 @@ it do expect(response).to have_http_status(:unprocessable_entity) expect(response_json['errors'].size).to eq(1) - expect(response_json['errors'][0]['status']).to eq('422') - expect(response_json['errors'][0]['code']).to include('blank') - expect(response_json['errors'][0]['title']) - .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) - expect(response_json['errors'][0]['source']) - .to eq('pointer' => '/data/relationships/user') - if Rails.gem_version >= Gem::Version.new('6.1') - expect(response_json['errors'][0]['detail']) - .to eq('User must exist') + expected_detail = if Rails.gem_version >= Gem::Version.new('6.1') + 'User must exist' else - expect(response_json['errors'][0]['detail']) - .to eq('User can\'t be blank') + 'User can\'t be blank' end + expect(response_json['errors']).to contain_exactly( + { + 'status' => '422', + 'source' => { 'pointer' => '/data/relationships/user' }, + 'title' => 'Unprocessable Entity', + 'detail' => expected_detail, + 'code' => 'blank' + } + ) end context 'required by validations' do @@ -81,45 +81,51 @@ it do expect(response).to have_http_status(:unprocessable_entity) expect(response_json['errors'].size).to eq(3) - expect(response_json['errors'][0]['status']).to eq('422') - expect(response_json['errors'][0]['code']).to include('invalid') - expect(response_json['errors'][0]['title']) - .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) - expect(response_json['errors'][0]['source']) - .to eq('pointer' => '/data/attributes/title') - expect(response_json['errors'][0]['detail']) - .to eq('Title is invalid') - - expect(response_json['errors'][1]['status']).to eq('422') - - if Rails::VERSION::MAJOR >= 5 - expect(response_json['errors'][1]['code']).to eq('invalid') - else - expect(response_json['errors'][1]['code']).to eq('has_typos') - end - - expect(response_json['errors'][1]['title']) - .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) - expect(response_json['errors'][1]['source']) - .to eq('pointer' => '/data/attributes/title') - expect(response_json['errors'][1]['detail']) - .to eq('Title has typos') - - expect(response_json['errors'][2]['status']).to eq('422') - - if Rails::VERSION::MAJOR >= 5 - expect(response_json['errors'][2]['code']).to eq('less_than') - else - expect(response_json['errors'][2]['code']) - .to eq('must_be_less_than_100') - end - - expect(response_json['errors'][2]['title']) - .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) - expect(response_json['errors'][2]['source']) - .to eq('pointer' => '/data/attributes/quantity') - expect(response_json['errors'][2]['detail']) - .to eq('Quantity must be less than 100') + expect(response_json['errors']).to contain_exactly( + { + 'status' => '422', + 'source' => { 'pointer' => '/data/attributes/title' }, + 'title' => 'Unprocessable Entity', + 'detail' => 'Title is invalid', + 'code' => 'invalid' + }, + { + 'status' => '422', + 'source' => { 'pointer' => '/data/attributes/title' }, + 'title' => 'Unprocessable Entity', + 'detail' => 'Title has typos', + 'code' => 'invalid' + }, + { + 'status' => '422', + 'source' => { 'pointer' => '/data/attributes/quantity' }, + 'title' => 'Unprocessable Entity', + 'detail' => 'Quantity must be less than 100', + 'code' => 'less_than' + } + ) + end + end + + context 'validations with non-interpolated messages' do + let(:params) do + payload = note_params.dup + payload[:data][:attributes][:title] = 'SLURS ARE GREAT' + payload + end + + it do + expect(response).to have_http_status(:unprocessable_entity) + expect(response_json['errors'].size).to eq(1) + expect(response_json['errors']).to contain_exactly( + { + 'status' => '422', + 'source' => { 'pointer' => '' }, + 'title' => 'Unprocessable Entity', + 'detail' => 'Title has slurs', + 'code' => 'title_has_slurs' + } + ) end end @@ -134,8 +140,15 @@ it do expect(response).to have_http_status(:unprocessable_entity) - expect(response_json['errors'][0]['source']) - .to eq('pointer' => '/data/attributes/title') + expect(response_json['errors']).to contain_exactly( + { + 'status' => '422', + 'source' => { 'pointer' => '/data/attributes/title' }, + 'title' => 'Unprocessable Entity', + 'detail' => nil, + 'code' => nil + } + ) end end end @@ -147,11 +160,15 @@ it do expect(response).to have_http_status(:not_found) expect(response_json['errors'].size).to eq(1) - expect(response_json['errors'][0]['status']).to eq('404') - expect(response_json['errors'][0]['title']) - .to eq(Rack::Utils::HTTP_STATUS_CODES[404]) - expect(response_json['errors'][0]['source']).to be_nil - expect(response_json['errors'][0]['detail']).to be_nil + expect(response_json['errors']).to contain_exactly( + { + 'status' => '404', + 'source' => nil, + 'title' => 'Not Found', + 'detail' => nil, + 'code' => nil + } + ) end end @@ -162,11 +179,15 @@ it do expect(response).to have_http_status(:internal_server_error) expect(response_json['errors'].size).to eq(1) - expect(response_json['errors'][0]['status']).to eq('500') - expect(response_json['errors'][0]['title']) - .to eq(Rack::Utils::HTTP_STATUS_CODES[500]) - expect(response_json['errors'][0]['source']).to be_nil - expect(response_json['errors'][0]['detail']).to be_nil + expect(response_json['errors']).to contain_exactly( + { + 'status' => '500', + 'source' => nil, + 'title' => 'Internal Server Error', + 'detail' => nil, + 'code' => nil + } + ) end end end From 5f52cddf274595ae5fa450a5f259390cc83519e2 Mon Sep 17 00:00:00 2001 From: Martin Meyerhoff Date: Sun, 23 Jun 2024 14:14:31 +0200 Subject: [PATCH 46/53] Allow decorating objects after pagination (#91) * Rubocop fix: Use filter_map instead of map { ... }.compact Currently, builds are failing because of Rubocop complaining about these three lines. * Fix specs with Ransack 4.0 Ransack 4 requires explicitly allowlisting ransackable attributes and associations for each model. This causes the specs to fail with the most recent Ransack version. This fixes those spec failures. * Allow decorating objects after pagination This simplification allows decorating objects after they are paginated, without losing the correct total object count. I'm using an instance variable on the including controller here, because the decorating the paginated collection will have us lose the instance variable we set on it. Here's the case where this happens: We have a complex ActiveRecord collection that we run through Ransack and Kaminari, but before rendering we want to convert each object in it using a `SimpleDelegator`. Here's a simplified version of the controller action we're looking at: ``` class UserDecorator < SimpleDelegator def fantastic_for_rendering "Whoah" end end def index allowed_fields = [ :first_name, :last_name, :created_at, :notes_created_at, :notes_quantity ] options = { sort_with_expressions: true } jsonapi_filter(User.all, allowed_fields, options) do |filtered| result = filtered.result jsonapi_paginate(result) do |paginated| paginated = paginated.map { |user| UserDecorator.new() } render jsonapi: paginated end end end ``` --------- Co-authored-by: Stas --- lib/jsonapi/pagination.rb | 15 ++++----------- spec/dummy.rb | 1 + spec/pagination_spec.rb | 23 ++++++++++++++++++++++- 3 files changed, 27 insertions(+), 12 deletions(-) diff --git a/lib/jsonapi/pagination.rb b/lib/jsonapi/pagination.rb index a98d149..5502e17 100644 --- a/lib/jsonapi/pagination.rb +++ b/lib/jsonapi/pagination.rb @@ -13,14 +13,13 @@ module Pagination def jsonapi_paginate(resources) offset, limit, _ = jsonapi_pagination_params + # Cache the original resources size to be used for pagination meta + @_jsonapi_original_size = resources.size + if resources.respond_to?(:offset) resources = resources.offset(offset).limit(limit) else - original_size = resources.size resources = resources[(offset)..(offset + limit - 1)] || [] - - # Cache the original resources size to be used for pagination meta - resources.instance_variable_set(:@original_size, original_size) end block_given? ? yield(resources) : resources @@ -64,13 +63,7 @@ def jsonapi_pagination_meta(resources) numbers = { current: page } - if resources.respond_to?(:unscope) - total = resources.unscope(:limit, :offset, :order).size - else - # Try to fetch the cached size first - total = resources.instance_variable_get(:@original_size) - total ||= resources.size - end + total = @_jsonapi_original_size last_page = [1, (total.to_f / limit).ceil].max diff --git a/spec/dummy.rb b/spec/dummy.rb index 8f222fd..c2e32ef 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -119,6 +119,7 @@ def index result = result.to_a if params[:as_list] jsonapi_paginate(result) do |paginated| + paginated = paginated.to_a if params[:decorate_after_pagination] render jsonapi: paginated end end diff --git a/spec/pagination_spec.rb b/spec/pagination_spec.rb index 622c81e..d417274 100644 --- a/spec/pagination_spec.rb +++ b/spec/pagination_spec.rb @@ -53,11 +53,13 @@ context 'on page 2 out of 3' do let(:as_list) { } + let(:decorate_after_pagination) { } let(:params) do { page: { number: 2, size: 1 }, sort: '-created_at', - as_list: as_list + as_list: as_list, + decorate_after_pagination: decorate_after_pagination }.compact_blank end @@ -80,6 +82,25 @@ end end + context 'when decorating objects after pagination' do + let(:decorate_after_pagination) { true } + + it do + expect(response).to have_http_status(:ok) + expect(response_json['data'].size).to eq(1) + expect(response_json['data'][0]).to have_id(second_user.id.to_s) + + expect(response_json['meta']['pagination']).to eq( + 'current' => 2, + 'first' => 1, + 'prev' => 1, + 'next' => 3, + 'last' => 3, + 'records' => 3 + ) + end + end + it do expect(response).to have_http_status(:ok) expect(response_json['data'].size).to eq(1) From bac072728a0e5e1d8da3e5ef9ba338aa1b08c669 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Sun, 23 Jun 2024 13:19:57 +0100 Subject: [PATCH 47/53] Cleanup CI config. --- .github/workflows/ci.yml | 14 ++++++++------ jsonapi.rb.gemspec | 2 +- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d9e837c..3843747 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -5,14 +5,17 @@ on: [push, pull_request] jobs: ruby_rails_test_matrix: runs-on: ubuntu-latest + continue-on-error: ${{ matrix.experimental }} strategy: matrix: - ruby: ['3.0', '3.1', '3.2', '3.3'] - rails: ['6.1', '7.0', '7.1'] - exclude: - - ruby: 3.2 - rails: 6 + ruby: ['3.1', '3.2', '3.3'] + rails: ['6.1', '7.0.1'] + experimental: [false] + include: + - rails: '7.1' + ruby: '3.3' + experimental: true steps: - uses: actions/checkout@v4 @@ -29,6 +32,5 @@ jobs: rm -rf Gemfile.lock sudo apt-get update sudo apt-get install libsqlite3-dev - echo $RAILS_VERSION | grep -q '5' && export SQLITE3_VERSION='~> 1.3.6' bundle bundle exec rake diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index eec2dad..c112540 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -31,7 +31,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'ransack' spec.add_development_dependency 'railties', ENV['RAILS_VERSION'] spec.add_development_dependency 'activerecord', ENV['RAILS_VERSION'] - spec.add_development_dependency 'sqlite3', ENV['SQLITE3_VERSION'] + spec.add_development_dependency 'sqlite3', '~> 1.7' spec.add_development_dependency 'ffaker' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'rspec-rails' From 87b5d006fae69f9b9064bf138465cfda19109e3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Sun, 23 Jun 2024 14:18:04 +0100 Subject: [PATCH 48/53] Version bump. --- lib/jsonapi/version.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/version.rb b/lib/jsonapi/version.rb index e013a61..4546016 100644 --- a/lib/jsonapi/version.rb +++ b/lib/jsonapi/version.rb @@ -1,3 +1,3 @@ module JSONAPI - VERSION = '2.0.1' + VERSION = '2.1.1' end From e6f7501fac7f60ea88a8c1c22e640a70dce7e8a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Sun, 23 Jun 2024 14:35:42 +0100 Subject: [PATCH 49/53] Cleanup readme. --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index ef6b2dc..093335d 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,6 @@ I'm grateful for the following companies for supporting this project!

-

From 30f0149ecfbc0f508fb9fc3af666bd844812bb0f Mon Sep 17 00:00:00 2001 From: David Roy Date: Mon, 20 Jan 2025 14:06:40 +0000 Subject: [PATCH 50/53] updates specs to run on newer ruby versions --- jsonapi.rb.gemspec | 2 +- lib/jsonapi/errors.rb | 2 +- spec/dummy.rb | 1 - spec/errors_spec.rb | 14 +++++++------- 4 files changed, 9 insertions(+), 10 deletions(-) diff --git a/jsonapi.rb.gemspec b/jsonapi.rb.gemspec index c112540..2d82832 100644 --- a/jsonapi.rb.gemspec +++ b/jsonapi.rb.gemspec @@ -31,7 +31,7 @@ Gem::Specification.new do |spec| spec.add_development_dependency 'ransack' spec.add_development_dependency 'railties', ENV['RAILS_VERSION'] spec.add_development_dependency 'activerecord', ENV['RAILS_VERSION'] - spec.add_development_dependency 'sqlite3', '~> 1.7' + spec.add_development_dependency 'sqlite3', '~> 2.1' spec.add_development_dependency 'ffaker' spec.add_development_dependency 'rspec', '~> 3.0' spec.add_development_dependency 'rspec-rails' diff --git a/lib/jsonapi/errors.rb b/lib/jsonapi/errors.rb index 1ecca5e..5b25425 100644 --- a/lib/jsonapi/errors.rb +++ b/lib/jsonapi/errors.rb @@ -47,7 +47,7 @@ def render_jsonapi_not_found(exception) render jsonapi_errors: [error], status: :not_found end - # Unprocessable entity (422) error handler callback + # Unprocessable Content (422) error handler callback # # @param exception [Exception] instance to handle # @return [String] JSONAPI error response diff --git a/spec/dummy.rb b/spec/dummy.rb index c2e32ef..dd2c0ec 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -84,7 +84,6 @@ class UserSerializer end class Dummy < Rails::Application - secrets.secret_key_base = '_' config.hosts << 'www.example.com' if config.respond_to?(:hosts) routes.draw do diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index a704a63..c817a9c 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -36,7 +36,7 @@ { 'status' => '422', 'source' => { 'pointer' => '' }, - 'title' => 'Unprocessable Entity', + 'title' => 'Unprocessable Content', 'detail' => nil, 'code' => nil } @@ -63,7 +63,7 @@ { 'status' => '422', 'source' => { 'pointer' => '/data/relationships/user' }, - 'title' => 'Unprocessable Entity', + 'title' => 'Unprocessable Content', 'detail' => expected_detail, 'code' => 'blank' } @@ -85,21 +85,21 @@ { 'status' => '422', 'source' => { 'pointer' => '/data/attributes/title' }, - 'title' => 'Unprocessable Entity', + 'title' => 'Unprocessable Content', 'detail' => 'Title is invalid', 'code' => 'invalid' }, { 'status' => '422', 'source' => { 'pointer' => '/data/attributes/title' }, - 'title' => 'Unprocessable Entity', + 'title' => 'Unprocessable Content', 'detail' => 'Title has typos', 'code' => 'invalid' }, { 'status' => '422', 'source' => { 'pointer' => '/data/attributes/quantity' }, - 'title' => 'Unprocessable Entity', + 'title' => 'Unprocessable Content', 'detail' => 'Quantity must be less than 100', 'code' => 'less_than' } @@ -121,7 +121,7 @@ { 'status' => '422', 'source' => { 'pointer' => '' }, - 'title' => 'Unprocessable Entity', + 'title' => 'Unprocessable Content', 'detail' => 'Title has slurs', 'code' => 'title_has_slurs' } @@ -144,7 +144,7 @@ { 'status' => '422', 'source' => { 'pointer' => '/data/attributes/title' }, - 'title' => 'Unprocessable Entity', + 'title' => 'Unprocessable Content', 'detail' => nil, 'code' => nil } From 0e253191418eb10620f6b7e6ee77938b8fec6926 Mon Sep 17 00:00:00 2001 From: David Roy Date: Mon, 20 Jan 2025 14:17:22 +0000 Subject: [PATCH 51/53] unescapes request.fullpath to match the remainder of the output when the request.fullpath has been sent escaped --- lib/jsonapi/pagination.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/pagination.rb b/lib/jsonapi/pagination.rb index 5502e17..9c6ef65 100644 --- a/lib/jsonapi/pagination.rb +++ b/lib/jsonapi/pagination.rb @@ -29,7 +29,7 @@ def jsonapi_paginate(resources) # # @return [Array] def jsonapi_pagination(resources) - links = { self: request.base_url + request.fullpath } + links = { self: request.base_url + CGI.unescape(request.fullpath) } pagination = jsonapi_pagination_meta(resources) return links if pagination.blank? From 260166c7491c711d7cd8973565fcf1cf4131d261 Mon Sep 17 00:00:00 2001 From: Matthew Bass Date: Mon, 20 Jan 2025 16:34:56 -0500 Subject: [PATCH 52/53] Add meta field to error serializer Per the JSON:API spec, the meta field can optionally be included in error response objects. See https://jsonapi.org/format/#error-objects --- lib/jsonapi/error_serializer.rb | 2 +- spec/errors_spec.rb | 27 ++++++++++++++++++--------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/lib/jsonapi/error_serializer.rb b/lib/jsonapi/error_serializer.rb index cb384fa..3b6e10f 100644 --- a/lib/jsonapi/error_serializer.rb +++ b/lib/jsonapi/error_serializer.rb @@ -8,7 +8,7 @@ class ErrorSerializer set_type :error # Object/Hash attribute helpers. - [:status, :source, :title, :detail, :code].each do |attr_name| + [:status, :source, :title, :detail, :code, :meta].each do |attr_name| attribute attr_name do |object| object.try(attr_name) || object.try(:fetch, attr_name, nil) end diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index c817a9c..1320dbc 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -38,7 +38,8 @@ 'source' => { 'pointer' => '' }, 'title' => 'Unprocessable Content', 'detail' => nil, - 'code' => nil + 'code' => nil, + 'meta' => nil } ) end @@ -65,7 +66,8 @@ 'source' => { 'pointer' => '/data/relationships/user' }, 'title' => 'Unprocessable Content', 'detail' => expected_detail, - 'code' => 'blank' + 'code' => 'blank', + 'meta' => nil } ) end @@ -87,21 +89,24 @@ 'source' => { 'pointer' => '/data/attributes/title' }, 'title' => 'Unprocessable Content', 'detail' => 'Title is invalid', - 'code' => 'invalid' + 'code' => 'invalid', + 'meta' => nil }, { 'status' => '422', 'source' => { 'pointer' => '/data/attributes/title' }, 'title' => 'Unprocessable Content', 'detail' => 'Title has typos', - 'code' => 'invalid' + 'code' => 'invalid', + 'meta' => nil }, { 'status' => '422', 'source' => { 'pointer' => '/data/attributes/quantity' }, 'title' => 'Unprocessable Content', 'detail' => 'Quantity must be less than 100', - 'code' => 'less_than' + 'code' => 'less_than', + 'meta' => nil } ) end @@ -123,7 +128,8 @@ 'source' => { 'pointer' => '' }, 'title' => 'Unprocessable Content', 'detail' => 'Title has slurs', - 'code' => 'title_has_slurs' + 'code' => 'title_has_slurs', + 'meta' => nil } ) end @@ -146,7 +152,8 @@ 'source' => { 'pointer' => '/data/attributes/title' }, 'title' => 'Unprocessable Content', 'detail' => nil, - 'code' => nil + 'code' => nil, + 'meta' => nil } ) end @@ -166,7 +173,8 @@ 'source' => nil, 'title' => 'Not Found', 'detail' => nil, - 'code' => nil + 'code' => nil, + 'meta' => nil } ) end @@ -185,7 +193,8 @@ 'source' => nil, 'title' => 'Internal Server Error', 'detail' => nil, - 'code' => nil + 'code' => nil, + 'meta' => nil } ) end From b5c6b601ea354b45c290bc689ad3da66151c2165 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stas=20SU=C8=98COV?= Date: Mon, 12 Jan 2026 21:23:35 +0000 Subject: [PATCH 53/53] Updated ci matrix. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3843747..fbe277e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,8 +9,8 @@ jobs: strategy: matrix: - ruby: ['3.1', '3.2', '3.3'] - rails: ['6.1', '7.0.1'] + ruby: ['3.3', '3.3', '3.4', '4'] + rails: ['7.2', '8.0', '8'] experimental: [false] include: - rails: '7.1' @@ -18,7 +18,7 @@ jobs: experimental: true steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - uses: ruby/setup-ruby@v1 with: