rails_api_kit 1.0.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/LICENSE.txt +21 -0
- data/README.md +313 -0
- data/lib/api_kit/error_serializer.rb +96 -0
- data/lib/api_kit/errors.rb +75 -0
- data/lib/api_kit/fetching.rb +42 -0
- data/lib/api_kit/filtering.rb +103 -0
- data/lib/api_kit/pagination.rb +168 -0
- data/lib/api_kit/patches.rb +71 -0
- data/lib/api_kit/rails_app.rb +157 -0
- data/lib/api_kit/version.rb +3 -0
- data/lib/api_kit.rb +13 -0
- data/lib/rails_api_kit.rb +1 -0
- data/spec/dummy.rb +201 -0
- data/spec/errors_spec.rb +126 -0
- data/spec/fetching_spec.rb +171 -0
- data/spec/filtering_spec.rb +101 -0
- data/spec/pagination_spec.rb +335 -0
- data/spec/spec_helper.rb +87 -0
- data/spec/support/api_kit_rspec.rb +41 -0
- metadata +272 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 34e70026279699c8b1e1a1a3b4bcd5fe6e35ebd30114490eabc87db67444d84b
|
|
4
|
+
data.tar.gz: 1c24f1ad20cddc1340688d39d051caa659cfaaa35ef38e30e2b9c68603847f95
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 742002a691ca1ce8bf656e13be82c495aec57cd6741bf21e3e22293857f83d61a8308904b08a9910e2d76984ba48efedf196d052627d0e8c68300ee0cd474953
|
|
7
|
+
data.tar.gz: cf6e1acbb07f3e573f80122cad888e2409599b17f691c54933864ca49106357fa3ab3e8e58b862f5af232c13a5c555cbc3ef2be1067b57415913a6cc7af13f75
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2019 Stas Suscov
|
|
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,313 @@
|
|
|
1
|
+
# ApiKit
|
|
2
|
+
|
|
3
|
+
A lightweight Rails toolkit for building standardized, structured API responses with serialization, error handling, filtering, sorting, and pagination.
|
|
4
|
+
|
|
5
|
+
> Building clean, consistent APIs shouldn't be rocket science. ApiKit provides simple, powerful modules to get you up and running quickly.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
ApiKit offers a collection of lightweight modules that integrate seamlessly with your Rails controllers:
|
|
10
|
+
|
|
11
|
+
* **Object serialization** - Powered by Active Model Serializers
|
|
12
|
+
* **Error handling** - Standardized error responses for parameters, validation, and generic errors
|
|
13
|
+
* **Fetching** - Support for relationship includes and sparse fieldsets
|
|
14
|
+
* **Filtering & Sorting** - Advanced filtering and sorting powered by Ransack
|
|
15
|
+
* **Pagination** - Built-in pagination support with links and metadata
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
|
|
19
|
+
**Requirements:**
|
|
20
|
+
- Ruby 3.3.0 or higher
|
|
21
|
+
- Rails 8.0 or higher
|
|
22
|
+
|
|
23
|
+
Add this line to your application's Gemfile:
|
|
24
|
+
|
|
25
|
+
```ruby
|
|
26
|
+
gem "rails_api_kit"
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
And then execute:
|
|
30
|
+
|
|
31
|
+
$ bundle install
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### 1. Enable Rails Integration
|
|
36
|
+
|
|
37
|
+
Add this to an initializer:
|
|
38
|
+
|
|
39
|
+
```ruby
|
|
40
|
+
# config/initializers/rails_api_kit.rb
|
|
41
|
+
require "rails_api_kit"
|
|
42
|
+
|
|
43
|
+
ApiKit::RailsApp.install!
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
This registers the media type and renderers.
|
|
47
|
+
|
|
48
|
+
### 2. Basic Usage
|
|
49
|
+
|
|
50
|
+
```ruby
|
|
51
|
+
class UsersController < ApplicationController
|
|
52
|
+
include ApiKit::Filtering
|
|
53
|
+
include ApiKit::Pagination
|
|
54
|
+
|
|
55
|
+
def index
|
|
56
|
+
allowed_fields = [ :first_name, :last_name, :created_at ]
|
|
57
|
+
|
|
58
|
+
api_filter(User.all, allowed_fields) do |filtered|
|
|
59
|
+
api_paginate(filtered.result) do |paginated|
|
|
60
|
+
render api_paginate: paginated
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def show
|
|
66
|
+
user = User.find(params[:id])
|
|
67
|
+
render api: user
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 3. Create Serializers
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
class UserSerializer < ActiveModel::Serializer
|
|
76
|
+
attributes :id, :first_name, :last_name, :email, :created_at, :updated_at
|
|
77
|
+
|
|
78
|
+
has_many :posts
|
|
79
|
+
end
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## Core Modules
|
|
83
|
+
|
|
84
|
+
### ApiKit::Filtering
|
|
85
|
+
|
|
86
|
+
Provides powerful filtering and sorting using Ransack:
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
class UsersController < ApplicationController
|
|
90
|
+
include ApiKit::Filtering
|
|
91
|
+
|
|
92
|
+
def index
|
|
93
|
+
allowed_fields = [ :first_name, :last_name, :email, :posts_title ]
|
|
94
|
+
|
|
95
|
+
api_filter(User.all, allowed_fields) do |filtered|
|
|
96
|
+
render api: filtered.result
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
**Example requests:**
|
|
103
|
+
```bash
|
|
104
|
+
# Filter by first name containing "John"
|
|
105
|
+
GET /users?filter[first_name_cont]=John
|
|
106
|
+
|
|
107
|
+
# Sort by last name descending, then first name ascending
|
|
108
|
+
GET /users?sort=-last_name,first_name
|
|
109
|
+
|
|
110
|
+
# Complex filtering with relationships
|
|
111
|
+
GET /users?filter[posts_title_matches_any]=Ruby,Rails&sort=-created_at
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Sorting with Expressions
|
|
115
|
+
|
|
116
|
+
Enable aggregation expressions for advanced sorting:
|
|
117
|
+
|
|
118
|
+
```ruby
|
|
119
|
+
def index
|
|
120
|
+
allowed_fields = [ :first_name, :posts_count ]
|
|
121
|
+
options = { sort_with_expressions: true }
|
|
122
|
+
|
|
123
|
+
api_filter(User.joins(:posts), allowed_fields, options) do |filtered|
|
|
124
|
+
render api: filtered.result.group("users.id")
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
```bash
|
|
130
|
+
# Sort by post count
|
|
131
|
+
GET /users?sort=-posts_count_sum
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### ApiKit::Pagination
|
|
135
|
+
|
|
136
|
+
Handles pagination with standardized links:
|
|
137
|
+
|
|
138
|
+
```ruby
|
|
139
|
+
class UsersController < ApplicationController
|
|
140
|
+
include ApiKit::Pagination
|
|
141
|
+
|
|
142
|
+
def index
|
|
143
|
+
api_paginate(User.all) do |paginated|
|
|
144
|
+
render api_paginate: paginated
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
private
|
|
149
|
+
|
|
150
|
+
def api_meta(resources)
|
|
151
|
+
{
|
|
152
|
+
pagination: api_pagination_meta(resources),
|
|
153
|
+
total: resources.respond_to?(:count) ? resources.count : resources.size
|
|
154
|
+
}
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
**Example requests:**
|
|
160
|
+
```bash
|
|
161
|
+
# Get page 2 with 20 items per page
|
|
162
|
+
GET /users?page[number]=2&page[size]=20
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### ApiKit::Fetching
|
|
166
|
+
|
|
167
|
+
Supports relationship inclusion and sparse fieldsets:
|
|
168
|
+
|
|
169
|
+
```ruby
|
|
170
|
+
class UsersController < ApplicationController
|
|
171
|
+
include ApiKit::Fetching
|
|
172
|
+
|
|
173
|
+
def index
|
|
174
|
+
render api: User.all
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
private
|
|
178
|
+
|
|
179
|
+
def api_include
|
|
180
|
+
# Whitelist allowed includes
|
|
181
|
+
super & [ "posts", "profile" ]
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
**Example requests:**
|
|
187
|
+
```bash
|
|
188
|
+
# Include related posts
|
|
189
|
+
GET /users?include=posts
|
|
190
|
+
|
|
191
|
+
# Sparse fieldsets
|
|
192
|
+
GET /users?fields[user]=first_name,last_name
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
### ApiKit::Errors
|
|
196
|
+
|
|
197
|
+
Standardized error responses for common Rails exceptions:
|
|
198
|
+
|
|
199
|
+
```ruby
|
|
200
|
+
class UsersController < ApplicationController
|
|
201
|
+
include ApiKit::Errors
|
|
202
|
+
|
|
203
|
+
def create
|
|
204
|
+
user = User.new(user_params)
|
|
205
|
+
|
|
206
|
+
if user.save
|
|
207
|
+
render api: user, status: :created
|
|
208
|
+
else
|
|
209
|
+
render api_errors: user.errors, status: :unprocessable_entity
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def update
|
|
214
|
+
user = User.find(params[:id])
|
|
215
|
+
|
|
216
|
+
if user.update(user_params)
|
|
217
|
+
render api: user
|
|
218
|
+
else
|
|
219
|
+
render api_errors: user.errors, status: :unprocessable_entity
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
private
|
|
224
|
+
|
|
225
|
+
def user_params
|
|
226
|
+
params.require(:data).require(:attributes).permit(:first_name, :last_name, :email)
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def render_api_internal_server_error(exception)
|
|
230
|
+
# Custom exception handling (e.g., error tracking)
|
|
231
|
+
# Sentry.capture_exception(exception)
|
|
232
|
+
super(exception)
|
|
233
|
+
end
|
|
234
|
+
end
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Handled exceptions:**
|
|
238
|
+
- `StandardError` → 500 Internal Server Error
|
|
239
|
+
- `ActiveRecord::RecordNotFound` → 404 Not Found
|
|
240
|
+
- `ActionController::ParameterMissing` → 422 Unprocessable Entity
|
|
241
|
+
|
|
242
|
+
## Advanced Configuration
|
|
243
|
+
|
|
244
|
+
### Custom Serializer Resolution
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
class UsersController < ApplicationController
|
|
248
|
+
def index
|
|
249
|
+
render api: User.all, serializer_class: CustomUserSerializer
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
private
|
|
253
|
+
|
|
254
|
+
def api_serializer_class(resource, is_collection)
|
|
255
|
+
ApiKit::RailsApp.serializer_class(resource, is_collection)
|
|
256
|
+
rescue NameError
|
|
257
|
+
"#{resource.class.name}Serializer".constantize
|
|
258
|
+
end
|
|
259
|
+
end
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### Custom Page Size
|
|
263
|
+
|
|
264
|
+
```ruby
|
|
265
|
+
def api_page_size(pagination_params)
|
|
266
|
+
per_page = pagination_params[:size].to_i
|
|
267
|
+
return 30 if per_page < 1 || per_page > 100
|
|
268
|
+
per_page
|
|
269
|
+
end
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Serializer Parameters
|
|
273
|
+
|
|
274
|
+
```ruby
|
|
275
|
+
def api_serializer_params
|
|
276
|
+
{
|
|
277
|
+
current_user: current_user,
|
|
278
|
+
include_private: params[:include_private].present?
|
|
279
|
+
}
|
|
280
|
+
end
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
## Configuration
|
|
284
|
+
|
|
285
|
+
### Environment Variables
|
|
286
|
+
|
|
287
|
+
- `PAGINATION_LIMIT` - Default page size (default: 30)
|
|
288
|
+
|
|
289
|
+
### Dependencies
|
|
290
|
+
|
|
291
|
+
This gem leverages these excellent libraries:
|
|
292
|
+
- [Active Model Serializers](https://github.com/rails-api/active_model_serializers) - Object serialization
|
|
293
|
+
- [Ransack](https://github.com/activerecord-hackery/ransack) - Advanced filtering and sorting
|
|
294
|
+
|
|
295
|
+
## Contributing
|
|
296
|
+
|
|
297
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/iakbudak/api_kit
|
|
298
|
+
|
|
299
|
+
This project follows the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
|
|
300
|
+
|
|
301
|
+
## Development
|
|
302
|
+
|
|
303
|
+
After checking out the repo:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
bundle install
|
|
307
|
+
bundle exec rspec # Run tests
|
|
308
|
+
bundle exec rubocop # Check code style
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
## License
|
|
312
|
+
|
|
313
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
module ApiKit
|
|
2
|
+
# Serializer for JSON:API error responses
|
|
3
|
+
|
|
4
|
+
class ErrorSerializer
|
|
5
|
+
# The errors to be serialized
|
|
6
|
+
# @return [Array, ActiveModel::Errors] the errors to be serialized
|
|
7
|
+
attr_reader :errors
|
|
8
|
+
|
|
9
|
+
# Serialization options
|
|
10
|
+
# @return [Hash] serialization options
|
|
11
|
+
attr_reader :options
|
|
12
|
+
|
|
13
|
+
# Initialize the error serializer
|
|
14
|
+
#
|
|
15
|
+
# @param errors [ActiveModel::Errors, Array] errors to serialize
|
|
16
|
+
# @param options [Hash] serialization options
|
|
17
|
+
def initialize(errors, options = {})
|
|
18
|
+
@errors = if errors.is_a?(ActiveModel::Errors)
|
|
19
|
+
errors.errors
|
|
20
|
+
else
|
|
21
|
+
Array(errors)
|
|
22
|
+
end
|
|
23
|
+
@options = options
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Convert errors to JSON format
|
|
27
|
+
#
|
|
28
|
+
# @param args [Array] arguments passed to to_json
|
|
29
|
+
# @return [String] JSON representation of errors
|
|
30
|
+
def to_json(*args)
|
|
31
|
+
{ errors: serialized_errors }.to_json(*args)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Serialize errors into JSON:API format
|
|
37
|
+
#
|
|
38
|
+
# @return [Array<Hash>] array of serialized error objects
|
|
39
|
+
def serialized_errors
|
|
40
|
+
errors.map do |error|
|
|
41
|
+
if error.is_a?(Array) && error.size == 2
|
|
42
|
+
# Handle [attribute, error_hash] format from rails_app.rb
|
|
43
|
+
serialize_validation_error(error[0], error[1])
|
|
44
|
+
elsif error.is_a?(Hash)
|
|
45
|
+
# Handle direct hash format
|
|
46
|
+
serialize_hash_error(error)
|
|
47
|
+
else
|
|
48
|
+
# Handle generic errors
|
|
49
|
+
serialize_generic_error(error)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Serialize validation error from ActiveModel
|
|
55
|
+
#
|
|
56
|
+
# @param attribute [Symbol, String] the attribute with the error
|
|
57
|
+
# @param error [Hash] the error details
|
|
58
|
+
# @return [Hash] serialized error object
|
|
59
|
+
def serialize_validation_error(attribute, error)
|
|
60
|
+
{
|
|
61
|
+
status: error[:status] || "422",
|
|
62
|
+
code: error[:code] || "invalid",
|
|
63
|
+
title: error[:title] || "Error",
|
|
64
|
+
detail: error[:message],
|
|
65
|
+
attribute: attribute
|
|
66
|
+
}.compact
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Serialize hash-formatted error
|
|
70
|
+
#
|
|
71
|
+
# @param error [Hash] the error hash
|
|
72
|
+
# @return [Hash] serialized error object
|
|
73
|
+
def serialize_hash_error(error)
|
|
74
|
+
{
|
|
75
|
+
status: error[:status] || "422",
|
|
76
|
+
code: error[:code] || "invalid",
|
|
77
|
+
title: error[:title] || "Error",
|
|
78
|
+
detail: error[:detail] || error["detail"]
|
|
79
|
+
}.compact
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Serialize generic error object
|
|
83
|
+
#
|
|
84
|
+
# @param error [Object] the error object
|
|
85
|
+
# @return [Hash] serialized error object
|
|
86
|
+
def serialize_generic_error(error)
|
|
87
|
+
{
|
|
88
|
+
status: "422",
|
|
89
|
+
code: error.type,
|
|
90
|
+
title: "Error",
|
|
91
|
+
detail: error.full_message,
|
|
92
|
+
attribute: error.attribute
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require "rack/utils"
|
|
2
|
+
|
|
3
|
+
module ApiKit
|
|
4
|
+
# Helpers to handle some error responses
|
|
5
|
+
#
|
|
6
|
+
# Most of the exceptions are handled in Rails by [ActionDispatch] middleware
|
|
7
|
+
# See: https://api.rubyonrails.org/classes/ActionDispatch/ExceptionWrapper.html
|
|
8
|
+
module Errors
|
|
9
|
+
# Callback will register the error handlers
|
|
10
|
+
#
|
|
11
|
+
# @return [Module]
|
|
12
|
+
def self.included(base)
|
|
13
|
+
base.class_eval do
|
|
14
|
+
rescue_from(
|
|
15
|
+
StandardError,
|
|
16
|
+
with: :render_api_internal_server_error
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
rescue_from(
|
|
20
|
+
ActiveRecord::RecordNotFound,
|
|
21
|
+
with: :render_api_not_found
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
rescue_from(
|
|
25
|
+
ActionController::ParameterMissing,
|
|
26
|
+
with: :render_api_unprocessable_entity
|
|
27
|
+
)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
# Generic error handler callback
|
|
33
|
+
#
|
|
34
|
+
# @param exception [Exception] instance to handle
|
|
35
|
+
# @return [String] error response
|
|
36
|
+
def render_api_internal_server_error(exception)
|
|
37
|
+
error = {
|
|
38
|
+
status: "500",
|
|
39
|
+
code: "internal_server_error",
|
|
40
|
+
title: Rack::Utils::HTTP_STATUS_CODES[500],
|
|
41
|
+
detail: exception.message
|
|
42
|
+
}
|
|
43
|
+
render api_errors: [ error ], status: :internal_server_error
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Not found (404) error handler callback
|
|
47
|
+
#
|
|
48
|
+
# @param exception [Exception] instance to handle
|
|
49
|
+
# @return [String] error response
|
|
50
|
+
def render_api_not_found(exception)
|
|
51
|
+
error = {
|
|
52
|
+
status: "404",
|
|
53
|
+
code: "not_found",
|
|
54
|
+
title: Rack::Utils::HTTP_STATUS_CODES[404],
|
|
55
|
+
detail: "Resource not found"
|
|
56
|
+
}
|
|
57
|
+
render api_errors: [ error ], status: :not_found
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Unprocessable entity (422) error handler callback
|
|
61
|
+
#
|
|
62
|
+
# @param exception [Exception] instance to handle
|
|
63
|
+
# @return [String] error response
|
|
64
|
+
def render_api_unprocessable_entity(exception)
|
|
65
|
+
error = {
|
|
66
|
+
status: "422",
|
|
67
|
+
code: "unprocessable_entity",
|
|
68
|
+
title: Rack::Utils::HTTP_STATUS_CODES[422],
|
|
69
|
+
detail: "Required parameter missing or invalid"
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
render api_errors: [ error ], status: :unprocessable_content
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module ApiKit
|
|
2
|
+
# Inclusion and sparse fields support
|
|
3
|
+
module Fetching
|
|
4
|
+
private
|
|
5
|
+
# Extracts and formats sparse fieldsets for Active Model Serializers
|
|
6
|
+
#
|
|
7
|
+
# Ex.: `GET /resource?fields[relationship]=id,created_at`
|
|
8
|
+
#
|
|
9
|
+
# @return [Hash] in Active Model Serializers format
|
|
10
|
+
def api_fields(serializer_class = nil, model_name = nil)
|
|
11
|
+
return unless params[:fields].respond_to?(:each_pair)
|
|
12
|
+
|
|
13
|
+
result = []
|
|
14
|
+
|
|
15
|
+
if serializer_class
|
|
16
|
+
model_name ||= serializer_class.name.demodulize.delete_suffix("Serializer").underscore
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
keys = []
|
|
20
|
+
params[:fields].each do |k, v|
|
|
21
|
+
field_names = v.to_s.split(",").map(&:strip).compact.map(&:to_sym)
|
|
22
|
+
result << { k.to_sym => field_names }
|
|
23
|
+
keys << k
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if model_name && serializer_class && !keys.include?(model_name)
|
|
27
|
+
result << { model_name.to_sym => serializer_class._attributes }
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
result
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extracts and whitelists allowed includes
|
|
34
|
+
#
|
|
35
|
+
# Ex.: `GET /resource?include=relationship,relationship.subrelationship`
|
|
36
|
+
#
|
|
37
|
+
# @return [Array]
|
|
38
|
+
def api_include
|
|
39
|
+
params["include"].to_s.split(",").map(&:strip).compact
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
require "ransack/predicate"
|
|
2
|
+
require_relative "patches"
|
|
3
|
+
|
|
4
|
+
# Filtering and sorting support
|
|
5
|
+
module ApiKit
|
|
6
|
+
module Filtering
|
|
7
|
+
# Parses and returns the attribute and the predicate of a ransack field
|
|
8
|
+
#
|
|
9
|
+
# @param requested_field [String] the field to parse
|
|
10
|
+
# @return [Array] with the fields and the predicate
|
|
11
|
+
def self.extract_attributes_and_predicates(requested_field)
|
|
12
|
+
predicates = []
|
|
13
|
+
field_name = requested_field.to_s.dup
|
|
14
|
+
|
|
15
|
+
while Ransack::Predicate.detect_from_string(field_name).present? do
|
|
16
|
+
predicate = Ransack::Predicate
|
|
17
|
+
.detect_and_strip_from_string!(field_name)
|
|
18
|
+
predicates << Ransack::Predicate.named(predicate)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
[ field_name.split(/_and_|_or_/), predicates.reverse ]
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
# Applies filtering and sorting to a set of resources if requested
|
|
26
|
+
#
|
|
27
|
+
# The fields follow [Ransack] specifications.
|
|
28
|
+
# See: https://github.com/activerecord-hackery/ransack#search-matchers
|
|
29
|
+
#
|
|
30
|
+
# Ex.: `GET /resource?filter[region_matches_any]=Lisb%&sort=-created_at,id`
|
|
31
|
+
#
|
|
32
|
+
# @param allowed_fields [Array] a list of allowed fields to be filtered
|
|
33
|
+
# @param options [Hash] extra flags to enable/disable features
|
|
34
|
+
# @return [ActiveRecord::Base] a collection of resources
|
|
35
|
+
def api_filter(resources, allowed_fields, options = {})
|
|
36
|
+
allowed_fields = allowed_fields.map(&:to_s)
|
|
37
|
+
extracted_params = api_filter_params(allowed_fields)
|
|
38
|
+
extracted_params[:sorts] = api_sort_params(allowed_fields, options)
|
|
39
|
+
resources = resources.ransack(extracted_params)
|
|
40
|
+
block_given? ? yield(resources) : resources
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Extracts and whitelists allowed fields to be filtered
|
|
44
|
+
#
|
|
45
|
+
# The fields follow [Ransack] specifications.
|
|
46
|
+
# See: https://github.com/activerecord-hackery/ransack#search-matchers
|
|
47
|
+
#
|
|
48
|
+
# @param allowed_fields [Array] a list of allowed fields to be filtered
|
|
49
|
+
# @return [Hash] to be passed to [ActiveRecord::Base#order]
|
|
50
|
+
def api_filter_params(allowed_fields)
|
|
51
|
+
filtered = {}
|
|
52
|
+
requested = params[:filter] || {}
|
|
53
|
+
allowed_fields = allowed_fields.map(&:to_s)
|
|
54
|
+
|
|
55
|
+
requested.each_pair do |requested_field, to_filter|
|
|
56
|
+
field_names, predicates = ApiKit::Filtering
|
|
57
|
+
.extract_attributes_and_predicates(requested_field)
|
|
58
|
+
|
|
59
|
+
wants_array = predicates.any? && predicates.map(&:wants_array).any?
|
|
60
|
+
|
|
61
|
+
if to_filter.is_a?(String) && wants_array
|
|
62
|
+
to_filter = to_filter.split(",")
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
if predicates.any? && (field_names - allowed_fields).empty?
|
|
66
|
+
filtered[requested_field] = to_filter
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
filtered
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Extracts and whitelists allowed fields (or expressions) to be sorted
|
|
74
|
+
#
|
|
75
|
+
# @param allowed_fields [Array] a list of allowed fields to be sorted
|
|
76
|
+
# @param options [Hash] extra options to enable/disable features
|
|
77
|
+
# @return [Hash] to be passed to [ActiveRecord::Base#order]
|
|
78
|
+
def api_sort_params(allowed_fields, options = {})
|
|
79
|
+
filtered = []
|
|
80
|
+
requested = params[:sort].to_s.split(",")
|
|
81
|
+
|
|
82
|
+
requested.each do |requested_field|
|
|
83
|
+
if requested_field.to_s.start_with?("-")
|
|
84
|
+
dir = "desc"
|
|
85
|
+
requested_field = requested_field[1..-1]
|
|
86
|
+
else
|
|
87
|
+
dir = "asc"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
field_names, predicates = ApiKit::Filtering
|
|
91
|
+
.extract_attributes_and_predicates(requested_field)
|
|
92
|
+
|
|
93
|
+
next unless (field_names - allowed_fields).empty?
|
|
94
|
+
next if !options[:sort_with_expressions] && predicates.any?
|
|
95
|
+
|
|
96
|
+
# Convert to strings instead of hashes to allow joined table columns.
|
|
97
|
+
filtered << [ requested_field, dir ].join(" ")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
filtered
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|