Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 10 additions & 9 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
9 changes: 9 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 0 additions & 2 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -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'
18 changes: 15 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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!
Expand Down Expand Up @@ -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`
Expand Down Expand Up @@ -290,6 +290,7 @@ class MyController < ActionController::Base
render jsonapi: paginated
end
end

end
```

Expand All @@ -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
Expand Down
6 changes: 5 additions & 1 deletion Rakefile
Original file line number Diff line number Diff line change
Expand Up @@ -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)
9 changes: 4 additions & 5 deletions jsonapi.rb.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand All @@ -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
3 changes: 0 additions & 3 deletions lib/jsonapi/active_model_error_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,6 @@
module JSONAPI
# [ActiveModel::Errors] serializer
class ActiveModelErrorSerializer < ErrorSerializer
set_id :object_id
set_type :error

attribute :status do
'422'
end
Expand Down
11 changes: 8 additions & 3 deletions lib/jsonapi/error_serializer.rb
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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]
Expand Down
30 changes: 25 additions & 5 deletions lib/jsonapi/pagination.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
Expand All @@ -82,23 +84,41 @@ def jsonapi_pagination_meta(resources)
numbers[:last] = last_page
end

if total.present?
numbers[:records] = total
end

numbers
end

# Extracts the pagination params
#
# @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]
Expand Down
46 changes: 37 additions & 9 deletions lib/jsonapi/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
Expand All @@ -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

Expand Down Expand Up @@ -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
Expand All @@ -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
2 changes: 1 addition & 1 deletion lib/jsonapi/version.rb
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
module JSONAPI
VERSION = '1.5.7'
VERSION = '1.7.0'
end
4 changes: 2 additions & 2 deletions spec/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,15 @@ def compound_validation
end

class CustomNoteSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer

set_type :note
belongs_to :user
attributes(:title, :quantity, :created_at, :updated_at)
end

class UserSerializer
include FastJsonapi::ObjectSerializer
include JSONAPI::Serializer

has_many :notes, serializer: CustomNoteSerializer
attributes(:last_name, :created_at, :updated_at)
Expand Down
9 changes: 7 additions & 2 deletions spec/errors_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading