railsful 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.gitignore +12 -0
- data/.reek.yml +2 -0
- data/.rspec +3 -0
- data/.travis.yml +18 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +116 -0
- data/Rakefile +6 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/railsful/deserializable.rb +13 -0
- data/lib/railsful/deserializer.rb +88 -0
- data/lib/railsful/exceptions.rb +37 -0
- data/lib/railsful/interceptors/errors.rb +61 -0
- data/lib/railsful/interceptors/include.rb +38 -0
- data/lib/railsful/interceptors/pagination.rb +105 -0
- data/lib/railsful/interceptors/sorting.rb +65 -0
- data/lib/railsful/railtie.rb +12 -0
- data/lib/railsful/serializable.rb +13 -0
- data/lib/railsful/serializer.rb +92 -0
- data/lib/railsful/version.rb +5 -0
- data/lib/railsful.rb +15 -0
- data/railsful.gemspec +42 -0
- metadata +180 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 77c58909e21f08d21955de91651f80db0b6e149351e2819e0980c8215f938fbb
|
4
|
+
data.tar.gz: ad03136f8c07f8c9d8b7db6000f1aec732c91b2c3f450ec6446ed882b0f00884
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1a8aab06a687afe8073b03541aa514e8e709983ea96b0f0e0802b08f93cabe4f3b71e70a465252b0b50e61288c1a300edda2433b9c2a7a2abae92d126a023696
|
7
|
+
data.tar.gz: 99ac9e457415aee433f83f20e48e483dc8176f2b8a73a6fabcd52ce8d1232e069a65deb8b30a89f2fb3d12526409d5d3a32d036a3e84092cfa45cb84780bc90f
|
data/.gitignore
ADDED
data/.reek.yml
ADDED
data/.rspec
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
sudo: false
|
2
|
+
env:
|
3
|
+
global:
|
4
|
+
- CC_TEST_REPORTER_ID=2002e6cb033fc09cb3a764c5d1622f756e0c2d2b61f0a62e51b8e38070cdff00
|
5
|
+
language: ruby
|
6
|
+
rvm:
|
7
|
+
- 2.6
|
8
|
+
- 2.5
|
9
|
+
- 2.4
|
10
|
+
- 2.3
|
11
|
+
before_script:
|
12
|
+
- curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter
|
13
|
+
- chmod +x ./cc-test-reporter
|
14
|
+
- ./cc-test-reporter before-build
|
15
|
+
script:
|
16
|
+
- bundle exec rspec
|
17
|
+
after_script:
|
18
|
+
- ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2018 Henning Vogt
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,116 @@
|
|
1
|
+
# Railsful
|
2
|
+
[](https://codeclimate.com/github/hausgold/railsful/maintainability)
|
3
|
+
[](https://codeclimate.com/github/hausgold/railsful/test_coverage)
|
4
|
+
|
5
|
+
A small but helpful collection of concerns and tools to create
|
6
|
+
a restful JSON API compliant Rails application.
|
7
|
+
|
8
|
+
## Installation
|
9
|
+
|
10
|
+
Add these lines to your application's Gemfile:
|
11
|
+
|
12
|
+
```ruby
|
13
|
+
# fast_jsonapi is used to serialize objects.
|
14
|
+
gem 'fast_jsonapi'
|
15
|
+
# kaminari needed for pagination.
|
16
|
+
gem 'kaminari'
|
17
|
+
|
18
|
+
gem 'railsful'
|
19
|
+
```
|
20
|
+
|
21
|
+
And then execute:
|
22
|
+
|
23
|
+
$ bundle
|
24
|
+
|
25
|
+
Or install it yourself as:
|
26
|
+
|
27
|
+
$ gem install railsful
|
28
|
+
|
29
|
+
## Usage
|
30
|
+
|
31
|
+
### Serialization
|
32
|
+
|
33
|
+
In order to serialize your objects it is necessary that their serializer follow
|
34
|
+
the same naming convention and modules as the object to be serialized.
|
35
|
+
|
36
|
+
``` ruby
|
37
|
+
# app/models/some_module/user.rb
|
38
|
+
module SomeModule
|
39
|
+
class User
|
40
|
+
...
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# app/serializers/some_module/user_serializer.rb
|
45
|
+
module SomeModule
|
46
|
+
class UserSerializer
|
47
|
+
...
|
48
|
+
end
|
49
|
+
end
|
50
|
+
```
|
51
|
+
|
52
|
+
After that you can use `render json: ...` without specifying the serializer:
|
53
|
+
|
54
|
+
``` ruby
|
55
|
+
module SomeModule
|
56
|
+
class UserController
|
57
|
+
def index
|
58
|
+
render json: User.all
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
```
|
63
|
+
|
64
|
+
Will result in:
|
65
|
+
|
66
|
+
``` json
|
67
|
+
GET /some_module/users
|
68
|
+
{
|
69
|
+
"data": [
|
70
|
+
{ "type": "user", "id": 1, "attributes": { ... } },
|
71
|
+
{ "type": "user", "id": 2, "attributes": { ... } }
|
72
|
+
]
|
73
|
+
}
|
74
|
+
```
|
75
|
+
|
76
|
+
### Deserialization
|
77
|
+
For deserialization of jsonapi compliant request all controllers that
|
78
|
+
inherit from `ActionController` can use the `#deserialized_params` method.
|
79
|
+
|
80
|
+
``` ruby
|
81
|
+
class UsersController < ApplicationController
|
82
|
+
def create
|
83
|
+
user = User.new(user_params)
|
84
|
+
|
85
|
+
# Return success/fail ...
|
86
|
+
end
|
87
|
+
|
88
|
+
private
|
89
|
+
|
90
|
+
def user_params
|
91
|
+
deserialized_params.permit(:first_name, :last_name, ...)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
```
|
95
|
+
|
96
|
+
## Development
|
97
|
+
|
98
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run
|
99
|
+
`rake spec` to run the tests. You can also run `bin/console` for an interactive
|
100
|
+
prompt that will allow you to experiment.
|
101
|
+
|
102
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To
|
103
|
+
release a new version, update the version number in `version.rb`, and then run
|
104
|
+
`bundle exec rake release`, which will create a git tag for the version, push
|
105
|
+
git commits and tags, and push the `.gem` file to
|
106
|
+
[rubygems.org](https://rubygems.org).
|
107
|
+
|
108
|
+
## Contributing
|
109
|
+
|
110
|
+
Bug reports and pull requests are welcome on GitHub at
|
111
|
+
https://github.com/hausgold/railsful.
|
112
|
+
|
113
|
+
## License
|
114
|
+
|
115
|
+
The gem is available as open source under the terms of the [MIT
|
116
|
+
License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "railsful"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'deep_merge/rails_compat'
|
4
|
+
|
5
|
+
module Railsful
|
6
|
+
class Deserializer
|
7
|
+
attr_reader :params
|
8
|
+
|
9
|
+
def initialize(params)
|
10
|
+
@params = params
|
11
|
+
end
|
12
|
+
|
13
|
+
# Deserializes the given params.
|
14
|
+
#
|
15
|
+
# :reek:FeatureEnvy
|
16
|
+
def deserialize
|
17
|
+
deserialized = {}
|
18
|
+
|
19
|
+
data = params.fetch(:data, {})
|
20
|
+
|
21
|
+
# Merge the resources attributes
|
22
|
+
deserialized.merge!(data.fetch(:attributes, {}))
|
23
|
+
|
24
|
+
# Get the already existing relationships
|
25
|
+
data.fetch(:relationships, {}).each do |type, payload|
|
26
|
+
deserialized.merge!(relationship(type, payload))
|
27
|
+
end
|
28
|
+
|
29
|
+
# Get the included elements.
|
30
|
+
deserialized.deeper_merge!(included_hash(params))
|
31
|
+
|
32
|
+
# Return the deserialized params.
|
33
|
+
ActionController::Parameters.new(deserialized)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Fetches all included associations/relationships from the
|
37
|
+
# included hash.
|
38
|
+
def included_hash(params)
|
39
|
+
included_hash = {}
|
40
|
+
|
41
|
+
params.fetch(:included, []).each do |inc|
|
42
|
+
type = inc[:type].to_sym
|
43
|
+
attrs = inc[:attributes]
|
44
|
+
|
45
|
+
if params.dig(:data, :relationships, type, :data).is_a?(Array)
|
46
|
+
# TODO: Pluralize the key here.
|
47
|
+
included_hash["#{type}_attributes"] ||= []
|
48
|
+
included_hash["#{type}_attributes"] << attrs
|
49
|
+
else
|
50
|
+
included_hash["#{type}_attributes"] = attrs
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
included_hash
|
55
|
+
end
|
56
|
+
|
57
|
+
def relationship(type, payload)
|
58
|
+
data = payload[:data]
|
59
|
+
|
60
|
+
return has_many_relationship(type, data) if data.is_a?(Array)
|
61
|
+
|
62
|
+
belongs_to_relationship(type, data)
|
63
|
+
end
|
64
|
+
|
65
|
+
# rubocop:disable Naming/PredicateName
|
66
|
+
def has_many_relationship(type, data)
|
67
|
+
return {} unless data.is_a?(Array)
|
68
|
+
|
69
|
+
ids = data.map { |relation| relation[:id] }.compact
|
70
|
+
|
71
|
+
return {} if ids.empty?
|
72
|
+
|
73
|
+
{ :"#{type}_ids" => ids }
|
74
|
+
end
|
75
|
+
# rubocop:enable Naming/PredicateName
|
76
|
+
|
77
|
+
def belongs_to_relationship(type, data)
|
78
|
+
# Fetch a possible id from the data.
|
79
|
+
relation_id = data[:id]
|
80
|
+
|
81
|
+
# If no ID is provided skip it.
|
82
|
+
return {} unless relation_id
|
83
|
+
|
84
|
+
# Build the relationship hash.
|
85
|
+
{ :"#{type}_id" => relation_id }
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Railsful
|
4
|
+
# The base error for this gem.
|
5
|
+
class Error < StandardError
|
6
|
+
attr_reader :detail, :status
|
7
|
+
|
8
|
+
# Initializer.
|
9
|
+
#
|
10
|
+
# @param detail [String] The detailed message.
|
11
|
+
# @param status [Integer] The status code.
|
12
|
+
def initialize(detail = nil, status = 400)
|
13
|
+
@detail = detail
|
14
|
+
@status = status
|
15
|
+
end
|
16
|
+
|
17
|
+
# Format the error as jsonapi wants it to.
|
18
|
+
#
|
19
|
+
# @return [Hash]
|
20
|
+
def as_json(_options = nil)
|
21
|
+
{
|
22
|
+
errors: [
|
23
|
+
{
|
24
|
+
status: status,
|
25
|
+
title: self.class.name.demodulize.underscore,
|
26
|
+
detail: detail
|
27
|
+
}
|
28
|
+
]
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# The error that is raised when pagination fails.
|
34
|
+
class PaginationError < Error; end
|
35
|
+
# The error that is raised when sorting fails.
|
36
|
+
class SortingError < Error; end
|
37
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'active_model/errors'
|
4
|
+
|
5
|
+
module Railsful
|
6
|
+
module Interceptors
|
7
|
+
# This interceptor checks the given json object for an 'errors' array
|
8
|
+
# and checks if any errors are available.
|
9
|
+
module Errors
|
10
|
+
def render(options)
|
11
|
+
super(errors_options(options))
|
12
|
+
end
|
13
|
+
|
14
|
+
def errors_options(options)
|
15
|
+
return options unless errors?(options)
|
16
|
+
|
17
|
+
# Fetch all the errors from the passed json value.
|
18
|
+
errors = errors(options.fetch(:json))
|
19
|
+
|
20
|
+
# Overwrite the json value and set the errors array.
|
21
|
+
options.merge(json: { errors: errors })
|
22
|
+
end
|
23
|
+
|
24
|
+
# Transform error output format into more "jsonapi" like.
|
25
|
+
def errors(raw_errors)
|
26
|
+
errors = []
|
27
|
+
|
28
|
+
raw_errors.details.each do |field, array|
|
29
|
+
errors += field_errors(field, array)
|
30
|
+
end
|
31
|
+
|
32
|
+
errors
|
33
|
+
end
|
34
|
+
|
35
|
+
def field_errors(field, array)
|
36
|
+
array.map do |hash|
|
37
|
+
formatted_error(hash, field)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Format the error by adding additional status and field information.
|
42
|
+
#
|
43
|
+
# :reek:UtilityFunction
|
44
|
+
def formatted_error(hash, field)
|
45
|
+
{
|
46
|
+
status: '422',
|
47
|
+
field: field
|
48
|
+
}.merge(hash)
|
49
|
+
end
|
50
|
+
|
51
|
+
# Checks if given renderable is an ActiveModel::Error
|
52
|
+
#
|
53
|
+
# :reek:UtilityFunction
|
54
|
+
def errors?(options)
|
55
|
+
return false unless options
|
56
|
+
|
57
|
+
options.fetch(:json, nil).is_a?(ActiveModel::Errors)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Railsful
|
4
|
+
module Interceptors
|
5
|
+
# This interceptors implements the "include" functionality for
|
6
|
+
# a given record or a relation.
|
7
|
+
module Include
|
8
|
+
def render(options)
|
9
|
+
super(include_options(options))
|
10
|
+
end
|
11
|
+
|
12
|
+
def include_options(options)
|
13
|
+
# Check if include key should be merged into options hash.
|
14
|
+
return options unless should_include?
|
15
|
+
|
16
|
+
# Deep merge include options, so we do not override existing
|
17
|
+
# include options.
|
18
|
+
options.deeper_merge(include: includes)
|
19
|
+
end
|
20
|
+
|
21
|
+
# Check if options should contain includes.
|
22
|
+
#
|
23
|
+
# @return [Boolean] The answer.
|
24
|
+
def should_include?
|
25
|
+
# Only GET requests should have the "include" functionality,
|
26
|
+
# since it may be a parameter in a create or update action.
|
27
|
+
method == 'GET' && includes.any?
|
28
|
+
end
|
29
|
+
|
30
|
+
# Fetch the list of all includes.
|
31
|
+
#
|
32
|
+
# @return [Array] The list of all include options.
|
33
|
+
def includes
|
34
|
+
params.fetch(:include, nil).to_s.split(',')
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Railsful
|
4
|
+
module Interceptors
|
5
|
+
# Interceptor that paginates a given ActiveRecord::Relation
|
6
|
+
# with help of the Kaminari gem.
|
7
|
+
module Pagination
|
8
|
+
def render(options)
|
9
|
+
super(pagination_options(options))
|
10
|
+
end
|
11
|
+
|
12
|
+
def pagination_options(options)
|
13
|
+
# Check if json value should be paginated.
|
14
|
+
return options unless paginate?(options)
|
15
|
+
|
16
|
+
# Get the relation from options hash so we can paginate it and
|
17
|
+
# check the total count.
|
18
|
+
relation = options.fetch(:json)
|
19
|
+
|
20
|
+
# Paginate the relation and store new relation in temporary variable.
|
21
|
+
paginated = paginate(relation)
|
22
|
+
|
23
|
+
# Create a meta hash
|
24
|
+
meta = {
|
25
|
+
total_pages: paginated.try(:total_pages),
|
26
|
+
total_count: paginated.try(:total_count),
|
27
|
+
current_page: paginated.try(:current_page),
|
28
|
+
next_page: paginated.try(:next_page),
|
29
|
+
prev_page: paginated.try(:prev_page)
|
30
|
+
}
|
31
|
+
|
32
|
+
options.deeper_merge(
|
33
|
+
links: links(paginated), meta: meta
|
34
|
+
).merge(json: paginated)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# Check if given entity is paginatable and request allows pagination.
|
40
|
+
#
|
41
|
+
# @param options [Hash] The global render options.
|
42
|
+
# @return [Boolean] The answer.
|
43
|
+
def paginate?(options)
|
44
|
+
method == 'GET' &&
|
45
|
+
params.fetch(:page, false) &&
|
46
|
+
relation?(options)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Paginate given relation
|
50
|
+
#
|
51
|
+
# @param relation [ActiveRecord::Relation] The relation.
|
52
|
+
# @return [ActiveRecord::Relation] The paginated relation.
|
53
|
+
def paginate(relation)
|
54
|
+
# If page param is not a hash, raise an error.
|
55
|
+
unless params.to_unsafe_hash.fetch(:page, nil).is_a?(Hash)
|
56
|
+
raise PaginationError,
|
57
|
+
'Wrong pagination format. Hash expected.'
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get the per page size.
|
61
|
+
per_page = params.dig(:page, :size)
|
62
|
+
|
63
|
+
relation = relation.page(params.dig(:page, :number))
|
64
|
+
relation = relation.per(per_page) if per_page
|
65
|
+
|
66
|
+
relation
|
67
|
+
end
|
68
|
+
|
69
|
+
# Create the pagination links
|
70
|
+
#
|
71
|
+
# @param relation [ActiveRecord::Relation] The relation to be paginated.
|
72
|
+
# @return [Hash] The +links+ hash.
|
73
|
+
def links(relation)
|
74
|
+
per_page = params.dig(:page, :size)
|
75
|
+
|
76
|
+
{
|
77
|
+
self: collection_url(relation.try(:current_page), per_page),
|
78
|
+
next: collection_url(relation.try(:next_page), per_page),
|
79
|
+
prev: collection_url(relation.try(:prev_page), per_page)
|
80
|
+
}
|
81
|
+
end
|
82
|
+
|
83
|
+
# The assembled pagination url.
|
84
|
+
#
|
85
|
+
# @param per_page [Integer] The per page count.
|
86
|
+
# @param page [Integer] The page.
|
87
|
+
# @return [String] The URL.
|
88
|
+
def collection_url(page, per_page)
|
89
|
+
return nil unless page
|
90
|
+
|
91
|
+
# Set fallback pagination.
|
92
|
+
# TODO: Get it from the model.
|
93
|
+
per_page ||= 25
|
94
|
+
|
95
|
+
# The +#url_for+ method comes from the given controller.
|
96
|
+
controller.url_for \
|
97
|
+
controller: params[:controller],
|
98
|
+
action: params[:action],
|
99
|
+
params: {
|
100
|
+
page: { number: page, size: per_page }
|
101
|
+
}
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Railsful
|
4
|
+
module Interceptors
|
5
|
+
# Interceptor that sorts a given ActiveRecord::Relation
|
6
|
+
module Sorting
|
7
|
+
def render(options)
|
8
|
+
super(sorting_options(options))
|
9
|
+
end
|
10
|
+
|
11
|
+
def sorting_options(options)
|
12
|
+
# Check if json value should be sorted.
|
13
|
+
return options unless sort?(options)
|
14
|
+
|
15
|
+
# Get the relation from options hash so we can sort it
|
16
|
+
relation = options.fetch(:json)
|
17
|
+
|
18
|
+
options.merge(json: sort(relation))
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
# Check if given entity is sortable and request allows sorting.
|
24
|
+
#
|
25
|
+
# @param options [Hash] The global render options.
|
26
|
+
# @return [Boolean] The answer.
|
27
|
+
def sort?(options)
|
28
|
+
method == 'GET' && params.fetch(:sort, false) && relation?(options)
|
29
|
+
end
|
30
|
+
|
31
|
+
# Format a sort string to a database friendly order string
|
32
|
+
#
|
33
|
+
# @return [String] database order query e.g. 'name DESC'
|
34
|
+
#
|
35
|
+
# :reek:UtilityFunction
|
36
|
+
def order(string)
|
37
|
+
string.start_with?('-') ? "#{string[1..-1]} DESC" : "#{string} ASC"
|
38
|
+
end
|
39
|
+
|
40
|
+
# Map the sort params to a database friendly set of strings
|
41
|
+
#
|
42
|
+
# @return [Array] Array of string e.g. ['name DESC', 'age ASC']
|
43
|
+
def orders
|
44
|
+
params.fetch(:sort).split(',').map do |string|
|
45
|
+
next unless string =~ /\A-?\w+\z/ # allow only word chars
|
46
|
+
|
47
|
+
order(string)
|
48
|
+
end.compact
|
49
|
+
end
|
50
|
+
|
51
|
+
# Sort given relation
|
52
|
+
#
|
53
|
+
# @param relation [ActiveRecord::Relation] The relation.
|
54
|
+
# @return [ActiveRecord::Relation] The sorted relation.
|
55
|
+
def sort(relation)
|
56
|
+
order_string = orders.join(', ')
|
57
|
+
# support both #reorder and #order call on relation
|
58
|
+
return relation.reorder(order_string) if relation.respond_to?(:reorder)
|
59
|
+
return relation.order(order_string) if relation.respond_to?(:order)
|
60
|
+
|
61
|
+
raise SortingError, 'Relation does not respond to #reorder or #order.'
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Railsful
|
4
|
+
class Railtie < Rails::Railtie
|
5
|
+
initializer 'railsful.action_controller' do
|
6
|
+
ActiveSupport.on_load(:action_controller) do
|
7
|
+
prepend Railsful::Serializable
|
8
|
+
include Railsful::Deserializable
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Railsful
|
4
|
+
module Serializable
|
5
|
+
def render(options = nil, extra_options = {}, &block)
|
6
|
+
super(fast_jsonapi_options(options), extra_options, &block)
|
7
|
+
end
|
8
|
+
|
9
|
+
def fast_jsonapi_options(options)
|
10
|
+
Serializer.new(self).render(options)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'railsful/interceptors/errors'
|
4
|
+
require 'railsful/interceptors/include'
|
5
|
+
require 'railsful/interceptors/pagination'
|
6
|
+
require 'railsful/interceptors/sorting'
|
7
|
+
|
8
|
+
module Railsful
|
9
|
+
# This class allows to encapsulate the interceptor logic from the
|
10
|
+
# prepended controller, so the controller is not polluted with
|
11
|
+
# all needed (helper) methods.
|
12
|
+
class Serializer
|
13
|
+
# All interceptors that provide jsonapi logic.
|
14
|
+
prepend Interceptors::Include
|
15
|
+
prepend Interceptors::Pagination
|
16
|
+
prepend Interceptors::Sorting
|
17
|
+
prepend Interceptors::Errors
|
18
|
+
|
19
|
+
attr_reader :controller
|
20
|
+
|
21
|
+
# Keep a reference to the controller, so all helper methods
|
22
|
+
# like +url_for+ can be used.
|
23
|
+
def initialize(controller)
|
24
|
+
@controller = controller
|
25
|
+
end
|
26
|
+
|
27
|
+
# The render function every interceptor MUST implement in order to
|
28
|
+
# add certain functionality.
|
29
|
+
#
|
30
|
+
# @param options [Hash] The render options hash.
|
31
|
+
# @return [Hash] The (modified) render options hash.
|
32
|
+
#
|
33
|
+
# :reek:FeatureEnvy
|
34
|
+
def render(options)
|
35
|
+
# Get the renderable (Object that should be rendered) from options hash.
|
36
|
+
renderable = options[:json]
|
37
|
+
|
38
|
+
# Return if renderable is blank
|
39
|
+
return options unless renderable
|
40
|
+
|
41
|
+
# Try to fetch the right serializer for given renderable.
|
42
|
+
serializer = serializer_for(renderable)
|
43
|
+
|
44
|
+
# When no serializer is found just pass the original options hash.
|
45
|
+
return options unless serializer
|
46
|
+
|
47
|
+
# Replace json value with new serializer
|
48
|
+
options.merge(json: serializer.new(renderable, options))
|
49
|
+
end
|
50
|
+
|
51
|
+
# Find the right serializer for given object.
|
52
|
+
#
|
53
|
+
# @param [ApplicationRecord, ActiveRecord::Relation]
|
54
|
+
# @return [Class] The serializer class.
|
55
|
+
#
|
56
|
+
# :reek:UtilityFunction
|
57
|
+
def serializer_for(renderable)
|
58
|
+
# Get the class in order to find the right serializer.
|
59
|
+
klass = if renderable.is_a?(ActiveRecord::Relation)
|
60
|
+
renderable.model.name
|
61
|
+
else
|
62
|
+
renderable.class.name
|
63
|
+
end
|
64
|
+
|
65
|
+
"#{klass}Serializer".safe_constantize
|
66
|
+
end
|
67
|
+
|
68
|
+
# Fetch the params from controller.
|
69
|
+
#
|
70
|
+
# @return [] The params.
|
71
|
+
def params
|
72
|
+
controller.params
|
73
|
+
end
|
74
|
+
|
75
|
+
# Fetch the HTTP method from controllers request.
|
76
|
+
#
|
77
|
+
# @return [String] The method.
|
78
|
+
def method
|
79
|
+
controller.request.request_method
|
80
|
+
end
|
81
|
+
|
82
|
+
# Check if given options contain an ActiveRecord::Relation.
|
83
|
+
#
|
84
|
+
# @param options [Hash] The options.
|
85
|
+
# @return [Boolean] The answer.
|
86
|
+
#
|
87
|
+
# :reek:UtilityFunction
|
88
|
+
def relation?(options)
|
89
|
+
options[:json].is_a?(ActiveRecord::Relation)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
data/lib/railsful.rb
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
require 'railsful/version'
|
4
|
+
require 'railsful/exceptions'
|
5
|
+
require 'railsful/railtie' if defined?(Rails)
|
6
|
+
require 'railsful/serializable'
|
7
|
+
require 'railsful/serializer'
|
8
|
+
require 'railsful/deserializable'
|
9
|
+
require 'railsful/deserializer'
|
10
|
+
|
11
|
+
# A small but helpful collection of concerns and tools to create
|
12
|
+
# a restful JSON API compliant Rails application.
|
13
|
+
module Railsful
|
14
|
+
# Nothing to do here
|
15
|
+
end
|
data/railsful.gemspec
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
lib = File.expand_path('lib', __dir__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
require 'railsful/version'
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'railsful'
|
7
|
+
spec.version = Railsful::VERSION
|
8
|
+
spec.authors = ['Henning Vogt', 'Marcus Geissler']
|
9
|
+
spec.email = ['henning.vogt@hausgold.de',
|
10
|
+
'marcus.geissler@hausgold.de']
|
11
|
+
|
12
|
+
spec.summary = 'JSON API serializer and deserializer for Rails'
|
13
|
+
spec.description = 'This gem provides useful helper functions to interact' \
|
14
|
+
'with JSON API.'
|
15
|
+
spec.homepage = 'https://github.com/hausgold/restful'
|
16
|
+
spec.license = 'MIT'
|
17
|
+
|
18
|
+
if spec.respond_to?(:metadata)
|
19
|
+
spec.metadata['allowed_push_host'] = 'https://rubygems.org'
|
20
|
+
else
|
21
|
+
raise 'RubyGems 2.0 or newer is required to protect against public pushes.'
|
22
|
+
end
|
23
|
+
|
24
|
+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
|
25
|
+
f.match(%r{^(test|spec|features)/})
|
26
|
+
end
|
27
|
+
|
28
|
+
spec.bindir = 'exe'
|
29
|
+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
30
|
+
spec.require_paths = ['lib']
|
31
|
+
|
32
|
+
spec.required_ruby_version = '>= 2.3'
|
33
|
+
|
34
|
+
spec.add_dependency 'deep_merge', '~> 1'
|
35
|
+
spec.add_dependency 'rails', ['>=4', '< 6']
|
36
|
+
|
37
|
+
spec.add_development_dependency 'bundler', '>= 1.16', '< 3'
|
38
|
+
spec.add_development_dependency 'pry'
|
39
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
40
|
+
spec.add_development_dependency 'rspec-rails', '~> 3.0'
|
41
|
+
spec.add_development_dependency 'simplecov', '~> 0.15'
|
42
|
+
end
|
metadata
ADDED
@@ -0,0 +1,180 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: railsful
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Henning Vogt
|
8
|
+
- Marcus Geissler
|
9
|
+
autorequire:
|
10
|
+
bindir: exe
|
11
|
+
cert_chain: []
|
12
|
+
date: 2019-01-14 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: deep_merge
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - "~>"
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '1'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - "~>"
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '1'
|
28
|
+
- !ruby/object:Gem::Dependency
|
29
|
+
name: rails
|
30
|
+
requirement: !ruby/object:Gem::Requirement
|
31
|
+
requirements:
|
32
|
+
- - ">="
|
33
|
+
- !ruby/object:Gem::Version
|
34
|
+
version: '4'
|
35
|
+
- - "<"
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: '6'
|
38
|
+
type: :runtime
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
requirements:
|
42
|
+
- - ">="
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '4'
|
45
|
+
- - "<"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '6'
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: bundler
|
50
|
+
requirement: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.16'
|
55
|
+
- - "<"
|
56
|
+
- !ruby/object:Gem::Version
|
57
|
+
version: '3'
|
58
|
+
type: :development
|
59
|
+
prerelease: false
|
60
|
+
version_requirements: !ruby/object:Gem::Requirement
|
61
|
+
requirements:
|
62
|
+
- - ">="
|
63
|
+
- !ruby/object:Gem::Version
|
64
|
+
version: '1.16'
|
65
|
+
- - "<"
|
66
|
+
- !ruby/object:Gem::Version
|
67
|
+
version: '3'
|
68
|
+
- !ruby/object:Gem::Dependency
|
69
|
+
name: pry
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
71
|
+
requirements:
|
72
|
+
- - ">="
|
73
|
+
- !ruby/object:Gem::Version
|
74
|
+
version: '0'
|
75
|
+
type: :development
|
76
|
+
prerelease: false
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
version: '0'
|
82
|
+
- !ruby/object:Gem::Dependency
|
83
|
+
name: rake
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
85
|
+
requirements:
|
86
|
+
- - "~>"
|
87
|
+
- !ruby/object:Gem::Version
|
88
|
+
version: '10.0'
|
89
|
+
type: :development
|
90
|
+
prerelease: false
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
92
|
+
requirements:
|
93
|
+
- - "~>"
|
94
|
+
- !ruby/object:Gem::Version
|
95
|
+
version: '10.0'
|
96
|
+
- !ruby/object:Gem::Dependency
|
97
|
+
name: rspec-rails
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
99
|
+
requirements:
|
100
|
+
- - "~>"
|
101
|
+
- !ruby/object:Gem::Version
|
102
|
+
version: '3.0'
|
103
|
+
type: :development
|
104
|
+
prerelease: false
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
106
|
+
requirements:
|
107
|
+
- - "~>"
|
108
|
+
- !ruby/object:Gem::Version
|
109
|
+
version: '3.0'
|
110
|
+
- !ruby/object:Gem::Dependency
|
111
|
+
name: simplecov
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
113
|
+
requirements:
|
114
|
+
- - "~>"
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
version: '0.15'
|
117
|
+
type: :development
|
118
|
+
prerelease: false
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - "~>"
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0.15'
|
124
|
+
description: This gem provides useful helper functions to interactwith JSON API.
|
125
|
+
email:
|
126
|
+
- henning.vogt@hausgold.de
|
127
|
+
- marcus.geissler@hausgold.de
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".reek.yml"
|
134
|
+
- ".rspec"
|
135
|
+
- ".travis.yml"
|
136
|
+
- Gemfile
|
137
|
+
- LICENSE.txt
|
138
|
+
- README.md
|
139
|
+
- Rakefile
|
140
|
+
- bin/console
|
141
|
+
- bin/setup
|
142
|
+
- lib/railsful.rb
|
143
|
+
- lib/railsful/deserializable.rb
|
144
|
+
- lib/railsful/deserializer.rb
|
145
|
+
- lib/railsful/exceptions.rb
|
146
|
+
- lib/railsful/interceptors/errors.rb
|
147
|
+
- lib/railsful/interceptors/include.rb
|
148
|
+
- lib/railsful/interceptors/pagination.rb
|
149
|
+
- lib/railsful/interceptors/sorting.rb
|
150
|
+
- lib/railsful/railtie.rb
|
151
|
+
- lib/railsful/serializable.rb
|
152
|
+
- lib/railsful/serializer.rb
|
153
|
+
- lib/railsful/version.rb
|
154
|
+
- railsful.gemspec
|
155
|
+
homepage: https://github.com/hausgold/restful
|
156
|
+
licenses:
|
157
|
+
- MIT
|
158
|
+
metadata:
|
159
|
+
allowed_push_host: https://rubygems.org
|
160
|
+
post_install_message:
|
161
|
+
rdoc_options: []
|
162
|
+
require_paths:
|
163
|
+
- lib
|
164
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
165
|
+
requirements:
|
166
|
+
- - ">="
|
167
|
+
- !ruby/object:Gem::Version
|
168
|
+
version: '2.3'
|
169
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
170
|
+
requirements:
|
171
|
+
- - ">="
|
172
|
+
- !ruby/object:Gem::Version
|
173
|
+
version: '0'
|
174
|
+
requirements: []
|
175
|
+
rubyforge_project:
|
176
|
+
rubygems_version: 2.7.6
|
177
|
+
signing_key:
|
178
|
+
specification_version: 4
|
179
|
+
summary: JSON API serializer and deserializer for Rails
|
180
|
+
test_files: []
|