rest_framework 0.8.15 → 0.8.17
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/VERSION +1 -1
- data/app/views/layouts/rest_framework.html.erb +149 -128
- data/app/views/rest_framework/_head.html.erb +249 -28
- data/app/views/rest_framework/_html_form.html.erb +55 -3
- data/app/views/rest_framework/_raw_form.html.erb +31 -3
- data/docs/CNAME +1 -0
- data/docs/Gemfile +4 -0
- data/docs/Gemfile.lock +264 -0
- data/docs/_config.yml +17 -0
- data/docs/_guide/1_routers.md +110 -0
- data/docs/_guide/2_controller_mixins.md +293 -0
- data/docs/_guide/3_serializers.md +60 -0
- data/docs/_guide/4_filtering_and_ordering.md +41 -0
- data/docs/_guide/5_pagination.md +21 -0
- data/docs/_includes/anchor_headings.html +144 -0
- data/docs/_includes/head.html +35 -0
- data/docs/_includes/header.html +58 -0
- data/docs/_layouts/default.html +11 -0
- data/docs/assets/css/rest_framework.css +159 -0
- data/docs/assets/images/favicon.ico +0 -0
- data/docs/assets/js/rest_framework.js +132 -0
- data/docs/index.md +133 -0
- data/lib/rest_framework/controller_mixins/base.rb +10 -3
- data/lib/rest_framework/controller_mixins/models.rb +120 -44
- data/lib/rest_framework/filters.rb +7 -9
- data/lib/rest_framework/serializers.rb +35 -12
- data/lib/rest_framework/utils.rb +11 -2
- data/lib/rest_framework/version.rb +4 -1
- data/lib/rest_framework.rb +2 -8
- metadata +22 -6
- data/app/views/rest_framework/_form_routes.html.erb +0 -10
data/docs/index.md
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
---
|
2
|
+
---
|
3
|
+
|
4
|
+
# Rails REST Framework
|
5
|
+
|
6
|
+
[![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
|
7
|
+
[![Pipeline](https://github.com/gregschmit/rails-rest-framework/actions/workflows/pipeline.yml/badge.svg)](https://github.com/gregschmit/rails-rest-framework/actions/workflows/pipeline.yml)
|
8
|
+
[![Coverage](https://coveralls.io/repos/github/gregschmit/rails-rest-framework/badge.svg?branch=master)](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
|
9
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/ba5df7706cb544d78555/maintainability)](https://codeclimate.com/github/gregschmit/rails-rest-framework/maintainability)
|
10
|
+
|
11
|
+
A framework for DRY RESTful APIs in Ruby on Rails.
|
12
|
+
|
13
|
+
**The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
|
14
|
+
logic, and routing them can be obnoxious. Building and maintaining features like ordering,
|
15
|
+
filtering, and pagination can be tedious.
|
16
|
+
|
17
|
+
**The Solution**: This framework implements browsable API responses, CRUD actions for your models,
|
18
|
+
and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
|
19
|
+
|
20
|
+
Website/Guide: [rails-rest-framework.com](https://rails-rest-framework.com)
|
21
|
+
|
22
|
+
Demo: [demo.rails-rest-framework.com](https://demo.rails-rest-framework.com)
|
23
|
+
|
24
|
+
Source: [github.com/gregschmit/rails-rest-framework](https://github.com/gregschmit/rails-rest-framework)
|
25
|
+
|
26
|
+
YARD Docs: [rubydoc.info/gems/rest_framework](https://rubydoc.info/gems/rest_framework)
|
27
|
+
|
28
|
+
## Installation
|
29
|
+
|
30
|
+
Add this line to your application's Gemfile:
|
31
|
+
|
32
|
+
```ruby
|
33
|
+
gem 'rest_framework'
|
34
|
+
```
|
35
|
+
|
36
|
+
And then execute:
|
37
|
+
|
38
|
+
```shell
|
39
|
+
$ bundle install
|
40
|
+
```
|
41
|
+
|
42
|
+
Or install it yourself with:
|
43
|
+
|
44
|
+
```shell
|
45
|
+
$ gem install rest_framework
|
46
|
+
```
|
47
|
+
|
48
|
+
## Quick Usage Tutorial
|
49
|
+
|
50
|
+
### Controller Mixins
|
51
|
+
|
52
|
+
To transform a controller into a RESTful controller, you can either include `BaseControllerMixin`,
|
53
|
+
`ReadOnlyModelControllerMixin`, or `ModelControllerMixin`. `BaseControllerMixin` provides a `root`
|
54
|
+
action and a simple interface for routing arbitrary additional actions:
|
55
|
+
|
56
|
+
```ruby
|
57
|
+
class ApiController < ApplicationController
|
58
|
+
include RESTFramework::BaseControllerMixin
|
59
|
+
self.extra_actions = {test: [:get]}
|
60
|
+
|
61
|
+
def test
|
62
|
+
render api_response({message: "Test successful!"})
|
63
|
+
end
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
`ModelControllerMixin` assists with providing the standard model CRUD for your controller.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
class Api::MoviesController < ApiController
|
71
|
+
include RESTFramework::ModelControllerMixin
|
72
|
+
|
73
|
+
self.recordset = Movie.where(enabled: true)
|
74
|
+
end
|
75
|
+
```
|
76
|
+
|
77
|
+
`ReadOnlyModelControllerMixin` only enables list/show actions, but since we're naming this
|
78
|
+
controller in a way that doesn't make the model obvious, we can set that explicitly:
|
79
|
+
|
80
|
+
```ruby
|
81
|
+
class Api::ReadOnlyMoviesController < ApiController
|
82
|
+
include RESTFramework::ReadOnlyModelControllerMixin
|
83
|
+
|
84
|
+
self.model = Movie
|
85
|
+
end
|
86
|
+
```
|
87
|
+
|
88
|
+
Note that you can also override the `get_recordset` instance method to override the API behavior
|
89
|
+
dynamically per-request.
|
90
|
+
|
91
|
+
### Routing
|
92
|
+
|
93
|
+
You can use Rails' `resource`/`resources` routers to route your API, however if you want
|
94
|
+
`extra_actions` / `extra_member_actions` to be routed automatically, then you can use `rest_route`
|
95
|
+
for non-resourceful controllers, or `rest_resource` / `rest_resources` resourceful routers. You can
|
96
|
+
also use `rest_root` to route the root of your API:
|
97
|
+
|
98
|
+
```ruby
|
99
|
+
Rails.application.routes.draw do
|
100
|
+
rest_root :api # will find `api_controller` and route the `root` action to '/api'
|
101
|
+
namespace :api do
|
102
|
+
rest_resources :movies
|
103
|
+
rest_resources :users
|
104
|
+
end
|
105
|
+
end
|
106
|
+
```
|
107
|
+
|
108
|
+
Or if you want the API root to be routed to `Api::RootController#root`:
|
109
|
+
|
110
|
+
```ruby
|
111
|
+
Rails.application.routes.draw do
|
112
|
+
namespace :api do
|
113
|
+
rest_root # will route `Api::RootController#root` to '/' in this namespace ('/api')
|
114
|
+
rest_resources :movies
|
115
|
+
rest_resources :users
|
116
|
+
end
|
117
|
+
end
|
118
|
+
```
|
119
|
+
|
120
|
+
## Development/Testing
|
121
|
+
|
122
|
+
After you clone the repository, cd'ing into the directory should create a new gemset if you are
|
123
|
+
using RVM. Then run `bundle install` to install the appropriate gems.
|
124
|
+
|
125
|
+
To run the test suite:
|
126
|
+
|
127
|
+
```shell
|
128
|
+
$ rails test
|
129
|
+
```
|
130
|
+
|
131
|
+
The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via
|
132
|
+
the usual commands. Ensure you run `rails db:setup` before running `rails server` or
|
133
|
+
`rails console`.
|
@@ -8,8 +8,16 @@ require_relative "../utils"
|
|
8
8
|
module RESTFramework::BaseControllerMixin
|
9
9
|
RRF_BASE_CONTROLLER_CONFIG = {
|
10
10
|
filter_pk_from_request_body: true,
|
11
|
-
exclude_body_fields: [
|
12
|
-
|
11
|
+
exclude_body_fields: %w[
|
12
|
+
created_at
|
13
|
+
created_by
|
14
|
+
created_by_id
|
15
|
+
updated_at
|
16
|
+
updated_by
|
17
|
+
updated_by_id
|
18
|
+
_method
|
19
|
+
utf8
|
20
|
+
authenticity_token
|
13
21
|
].freeze,
|
14
22
|
extra_actions: nil,
|
15
23
|
extra_member_actions: nil,
|
@@ -323,7 +331,6 @@ module RESTFramework::BaseControllerMixin
|
|
323
331
|
@json_payload = payload.to_json if self.class.serialize_to_json
|
324
332
|
@xml_payload = payload.to_xml if self.class.serialize_to_xml
|
325
333
|
end
|
326
|
-
@template_logo_text ||= "Rails REST Framework"
|
327
334
|
@title ||= self.class.get_title
|
328
335
|
@description ||= self.class.description
|
329
336
|
@route_props, @route_groups = RESTFramework::Utils.get_routes(
|
@@ -3,6 +3,15 @@ require_relative "../filters"
|
|
3
3
|
|
4
4
|
# This module provides the core functionality for controllers based on models.
|
5
5
|
module RESTFramework::BaseModelControllerMixin
|
6
|
+
BASE64_REGEX = /data:(.*);base64,(.*)/
|
7
|
+
BASE64_TRANSLATE = ->(field, value) {
|
8
|
+
_, content_type, payload = value.match(BASE64_REGEX).to_a
|
9
|
+
return {
|
10
|
+
io: StringIO.new(Base64.decode64(payload)),
|
11
|
+
content_type: content_type,
|
12
|
+
filename: "image_#{field}#{Rack::Mime::MIME_TYPES.invert[content_type]}",
|
13
|
+
}
|
14
|
+
}
|
6
15
|
include RESTFramework::BaseControllerMixin
|
7
16
|
|
8
17
|
RRF_BASE_MODEL_CONTROLLER_CONFIG = {
|
@@ -89,20 +98,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
89
98
|
return self.get_model.human_attribute_name(s, default: super)
|
90
99
|
end
|
91
100
|
|
92
|
-
# Get the available fields.
|
93
|
-
#
|
94
|
-
|
95
|
-
|
96
|
-
def get_fields(input_fields: nil, fallback: true)
|
97
|
-
input_fields ||= self.fields if fallback
|
101
|
+
# Get the available fields. Fallback to this controller's model columns, or an empty array. This
|
102
|
+
# should always return an array of strings.
|
103
|
+
def get_fields(input_fields: nil)
|
104
|
+
input_fields ||= self.fields
|
98
105
|
|
99
106
|
# If fields is a hash, then parse it.
|
100
107
|
if input_fields.is_a?(Hash)
|
101
108
|
return RESTFramework::Utils.parse_fields_hash(
|
102
109
|
input_fields, self.get_model, exclude_associations: self.exclude_associations
|
103
110
|
)
|
104
|
-
elsif !input_fields
|
105
|
-
# Otherwise, if fields is nil
|
111
|
+
elsif !input_fields
|
112
|
+
# Otherwise, if fields is nil, then fallback to columns.
|
106
113
|
model = self.get_model
|
107
114
|
return model ? RESTFramework::Utils.fields_for(
|
108
115
|
model, exclude_associations: self.exclude_associations
|
@@ -148,7 +155,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
148
155
|
end
|
149
156
|
|
150
157
|
# Get metadata about the resource's fields.
|
151
|
-
def
|
158
|
+
def fields_metadata
|
159
|
+
return @_fields_metadata if @_fields_metadata
|
160
|
+
|
152
161
|
# Get metadata sources.
|
153
162
|
model = self.get_model
|
154
163
|
fields = self.get_fields.map(&:to_s)
|
@@ -156,8 +165,14 @@ module RESTFramework::BaseModelControllerMixin
|
|
156
165
|
column_defaults = model.column_defaults
|
157
166
|
reflections = model.reflections
|
158
167
|
attributes = model._default_attributes
|
159
|
-
|
160
|
-
|
168
|
+
readonly_attributes = model.readonly_attributes
|
169
|
+
exclude_body_fields = self.exclude_body_fields.map(&:to_s)
|
170
|
+
rich_text_association_names = model.reflect_on_all_associations(:has_one)
|
171
|
+
.collect(&:name)
|
172
|
+
.select { |n| n.to_s.start_with?("rich_text_") }
|
173
|
+
attachment_reflections = model.attachment_reflections
|
174
|
+
|
175
|
+
return @_fields_metadata = fields.map { |f|
|
161
176
|
# Initialize metadata to make the order consistent.
|
162
177
|
metadata = {
|
163
178
|
type: nil,
|
@@ -173,6 +188,11 @@ module RESTFramework::BaseModelControllerMixin
|
|
173
188
|
metadata[:primary_key] = true
|
174
189
|
end
|
175
190
|
|
191
|
+
# Determine if the field is a read-only attribute.
|
192
|
+
if metadata[:primary_key] || f.in?(readonly_attributes) || f.in?(exclude_body_fields)
|
193
|
+
metadata[:read_only] = true
|
194
|
+
end
|
195
|
+
|
176
196
|
# Determine `type`, `required`, `label`, and `kind` based on schema.
|
177
197
|
if column = columns[f]
|
178
198
|
metadata[:kind] = "column"
|
@@ -249,9 +269,22 @@ module RESTFramework::BaseModelControllerMixin
|
|
249
269
|
}.compact
|
250
270
|
end
|
251
271
|
|
272
|
+
# Determine if this is an ActionText "rich text".
|
273
|
+
if :"rich_text_#{f}".in?(rich_text_association_names)
|
274
|
+
metadata[:kind] = "rich_text"
|
275
|
+
end
|
276
|
+
|
277
|
+
# Determine if this is an ActiveStorage attachment.
|
278
|
+
if ref = attachment_reflections[f]
|
279
|
+
metadata[:kind] = "attachment"
|
280
|
+
metadata[:attachment] = {
|
281
|
+
macro: ref.macro,
|
282
|
+
}
|
283
|
+
end
|
284
|
+
|
252
285
|
# Determine if this is just a method.
|
253
|
-
if model.method_defined?(f)
|
254
|
-
metadata[:kind]
|
286
|
+
if !metadata[:kind] && model.method_defined?(f)
|
287
|
+
metadata[:kind] = "method"
|
255
288
|
end
|
256
289
|
|
257
290
|
# Collect validator options into a hash on their type, while also updating `required` based
|
@@ -290,7 +323,8 @@ module RESTFramework::BaseModelControllerMixin
|
|
290
323
|
def get_options_metadata
|
291
324
|
return super.merge(
|
292
325
|
{
|
293
|
-
|
326
|
+
primary_key: self.get_model.primary_key,
|
327
|
+
fields: self.fields_metadata,
|
294
328
|
callbacks: self._process_action_callbacks.as_json,
|
295
329
|
},
|
296
330
|
)
|
@@ -377,9 +411,9 @@ module RESTFramework::BaseModelControllerMixin
|
|
377
411
|
end
|
378
412
|
|
379
413
|
# Get a list of fields, taking into account the current action.
|
380
|
-
def get_fields
|
414
|
+
def get_fields
|
381
415
|
fields = self._get_specific_action_config(:action_fields, :fields)
|
382
|
-
return self.class.get_fields(input_fields: fields
|
416
|
+
return self.class.get_fields(input_fields: fields)
|
383
417
|
end
|
384
418
|
|
385
419
|
# Pass fields to get dynamic metadata based on which fields are available.
|
@@ -387,14 +421,12 @@ module RESTFramework::BaseModelControllerMixin
|
|
387
421
|
return self.class.get_options_metadata
|
388
422
|
end
|
389
423
|
|
390
|
-
# Get a list of find_by fields for the current action.
|
391
|
-
# wants to find by virtual columns.
|
424
|
+
# Get a list of find_by fields for the current action.
|
392
425
|
def get_find_by_fields
|
393
|
-
return self.class.find_by_fields
|
426
|
+
return self.class.find_by_fields
|
394
427
|
end
|
395
428
|
|
396
|
-
# Get a list of parameters allowed for the current action.
|
397
|
-
# columns so arbitrary fields can be submitted if no fields are defined.
|
429
|
+
# Get a list of parameters allowed for the current action.
|
398
430
|
def get_allowed_parameters
|
399
431
|
return @_get_allowed_parameters if defined?(@_get_allowed_parameters)
|
400
432
|
|
@@ -403,35 +435,54 @@ module RESTFramework::BaseModelControllerMixin
|
|
403
435
|
:allowed_parameters,
|
404
436
|
)
|
405
437
|
return @_get_allowed_parameters if @_get_allowed_parameters
|
406
|
-
return @_get_allowed_parameters = nil unless fields = self.get_fields
|
407
438
|
|
408
439
|
# For fields, automatically add `_id`/`_ids` and `_attributes` variations for associations.
|
409
|
-
|
410
|
-
|
411
|
-
|
440
|
+
variations = []
|
441
|
+
hash_variations = {}
|
442
|
+
reflections = self.class.get_model.reflections
|
443
|
+
@_get_allowed_parameters = self.get_fields.map { |f|
|
412
444
|
f = f.to_s
|
413
|
-
|
445
|
+
|
446
|
+
# ActiveStorage Integration: `has_one_attached`.
|
447
|
+
if reflections.key?("#{f}_attachment")
|
448
|
+
next f
|
449
|
+
end
|
450
|
+
|
451
|
+
# ActiveStorage Integration: `has_many_attached`.
|
452
|
+
if reflections.key?("#{f}_attachments")
|
453
|
+
hash_variations[f] = []
|
454
|
+
next nil
|
455
|
+
end
|
456
|
+
|
457
|
+
# ActionText Integration.
|
458
|
+
if reflections.key?("rich_test_#{f}")
|
459
|
+
next f
|
460
|
+
end
|
461
|
+
|
462
|
+
# Return field if it's not an association.
|
463
|
+
next f unless ref = reflections[f]
|
414
464
|
|
415
465
|
if self.class.permit_id_assignment
|
416
466
|
if ref.collection?
|
417
|
-
|
467
|
+
hash_variations["#{f.singularize}_ids"] = []
|
418
468
|
elsif ref.belongs_to?
|
419
|
-
|
469
|
+
variations << "#{f}_id"
|
420
470
|
end
|
421
471
|
end
|
422
472
|
|
423
473
|
if self.class.permit_nested_attributes_assignment
|
424
474
|
if self.class.allow_all_nested_attributes
|
425
|
-
|
475
|
+
hash_variations["#{f}_attributes"] = {}
|
426
476
|
else
|
427
|
-
|
477
|
+
hash_variations["#{f}_attributes"] = self.class.get_field_config(f)[:sub_fields]
|
428
478
|
end
|
429
479
|
end
|
430
480
|
|
431
|
-
|
432
|
-
|
433
|
-
|
434
|
-
@_get_allowed_parameters
|
481
|
+
# Associations are not allowed to be submitted in their bare form.
|
482
|
+
next nil
|
483
|
+
}.compact
|
484
|
+
@_get_allowed_parameters += variations
|
485
|
+
@_get_allowed_parameters << hash_variations
|
435
486
|
return @_get_allowed_parameters
|
436
487
|
end
|
437
488
|
|
@@ -454,14 +505,8 @@ module RESTFramework::BaseModelControllerMixin
|
|
454
505
|
def get_body_params(data: nil)
|
455
506
|
data ||= request.request_parameters
|
456
507
|
|
457
|
-
# Filter the request body
|
458
|
-
|
459
|
-
body_params = if allowed_parameters = self.get_allowed_parameters
|
460
|
-
data = ActionController::Parameters.new(data)
|
461
|
-
data.permit(*allowed_parameters)
|
462
|
-
else
|
463
|
-
data
|
464
|
-
end
|
508
|
+
# Filter the request body with strong params.
|
509
|
+
body_params = ActionController::Parameters.new(data).permit(*self.get_allowed_parameters)
|
465
510
|
|
466
511
|
# Filter primary key if configured.
|
467
512
|
if self.class.filter_pk_from_request_body
|
@@ -471,6 +516,27 @@ module RESTFramework::BaseModelControllerMixin
|
|
471
516
|
# Filter fields in `exclude_body_fields`.
|
472
517
|
(self.class.exclude_body_fields || []).each { |f| body_params.delete(f) }
|
473
518
|
|
519
|
+
# ActiveStorage Integration: Translate base64 encoded attachments to upload objects.
|
520
|
+
#
|
521
|
+
# rubocop:disable Layout/LineLength
|
522
|
+
#
|
523
|
+
# Good example base64 image:
|
524
|
+
# data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABgAAAAYCAYAAADgdz34AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAApgAAAKYB3X3/OAAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAANCSURBVEiJtZZPbBtFFMZ/M7ubXdtdb1xSFyeilBapySVU8h8OoFaooFSqiihIVIpQBKci6KEg9Q6H9kovIHoCIVQJJCKE1ENFjnAgcaSGC6rEnxBwA04Tx43t2FnvDAfjkNibxgHxnWb2e/u992bee7tCa00YFsffekFY+nUzFtjW0LrvjRXrCDIAaPLlW0nHL0SsZtVoaF98mLrx3pdhOqLtYPHChahZcYYO7KvPFxvRl5XPp1sN3adWiD1ZAqD6XYK1b/dvE5IWryTt2udLFedwc1+9kLp+vbbpoDh+6TklxBeAi9TL0taeWpdmZzQDry0AcO+jQ12RyohqqoYoo8RDwJrU+qXkjWtfi8Xxt58BdQuwQs9qC/afLwCw8tnQbqYAPsgxE1S6F3EAIXux2oQFKm0ihMsOF71dHYx+f3NND68ghCu1YIoePPQN1pGRABkJ6Bus96CutRZMydTl+TvuiRW1m3n0eDl0vRPcEysqdXn+jsQPsrHMquGeXEaY4Yk4wxWcY5V/9scqOMOVUFthatyTy8QyqwZ+kDURKoMWxNKr2EeqVKcTNOajqKoBgOE28U4tdQl5p5bwCw7BWquaZSzAPlwjlithJtp3pTImSqQRrb2Z8PHGigD4RZuNX6JYj6wj7O4TFLbCO/Mn/m8R+h6rYSUb3ekokRY6f/YukArN979jcW+V/S8g0eT/N3VN3kTqWbQ428m9/8k0P/1aIhF36PccEl6EhOcAUCrXKZXXWS3XKd2vc/TRBG9O5ELC17MmWubD2nKhUKZa26Ba2+D3P+4/MNCFwg59oWVeYhkzgN/JDR8deKBoD7Y+ljEjGZ0sosXVTvbc6RHirr2reNy1OXd6pJsQ+gqjk8VWFYmHrwBzW/n+uMPFiRwHB2I7ih8ciHFxIkd/3Omk5tCDV1t+2nNu5sxxpDFNx+huNhVT3/zMDz8usXC3ddaHBj1GHj/As08fwTS7Kt1HBTmyN29vdwAw+/wbwLVOJ3uAD1wi/dUH7Qei66PfyuRj4Ik9is+hglfbkbfR3cnZm7chlUWLdwmprtCohX4HUtlOcQjLYCu+fzGJH2QRKvP3UNz8bWk1qMxjGTOMThZ3kvgLI5AzFfo379UAAAAASUVORK5CYII=
|
525
|
+
#
|
526
|
+
# rubocop:enable Layout/LineLength
|
527
|
+
self.class.get_model.attachment_reflections.keys.each do |k|
|
528
|
+
next unless (body_params[k].is_a?(String) && body_params[k].match?(BASE64_REGEX)) ||
|
529
|
+
(body_params[k].is_a?(Array) && body_params[k].all? { |v|
|
530
|
+
v.is_a?(String) && v.match?(BASE64_REGEX)
|
531
|
+
})
|
532
|
+
|
533
|
+
if body_params[k].is_a?(Array)
|
534
|
+
body_params[k] = body_params[k].map { |v| BASE64_TRANSLATE.call(k, v) }
|
535
|
+
else
|
536
|
+
body_params[k] = BASE64_TRANSLATE.call(k, body_params[k])
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
474
540
|
return body_params
|
475
541
|
end
|
476
542
|
alias_method :get_create_params, :get_body_params
|
@@ -492,8 +558,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
492
558
|
|
493
559
|
# Get the recordset but with any associations included to avoid N+1 queries.
|
494
560
|
def get_recordset_with_includes
|
495
|
-
reflections = self.class.get_model.reflections
|
496
|
-
associations = self.get_fields
|
561
|
+
reflections = self.class.get_model.reflections
|
562
|
+
associations = self.get_fields.map { |f|
|
563
|
+
if reflections.key?(f)
|
564
|
+
f.to_sym
|
565
|
+
elsif reflections.key?("rich_text_#{f}")
|
566
|
+
:"rich_text_#{f}"
|
567
|
+
elsif reflections.key?("#{f}_attachment")
|
568
|
+
:"#{f}_attachment"
|
569
|
+
elsif reflections.key?("#{f}_attachments")
|
570
|
+
:"#{f}_attachments"
|
571
|
+
end
|
572
|
+
}.compact
|
497
573
|
|
498
574
|
if associations.any?
|
499
575
|
return self.get_recordset.includes(associations)
|
@@ -11,11 +11,10 @@ end
|
|
11
11
|
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
12
12
|
# controller class.
|
13
13
|
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
14
|
-
# Get a list of filterset fields for the current action.
|
15
|
-
# to try filtering by any query parameter because that could clash with other query parameters.
|
14
|
+
# Get a list of filterset fields for the current action.
|
16
15
|
def _get_fields
|
17
16
|
# Always return a list of strings; `@controller.get_fields` already does this.
|
18
|
-
return @controller.class.filterset_fields&.map(&:to_s) || @controller.get_fields
|
17
|
+
return @controller.class.filterset_fields&.map(&:to_s) || @controller.get_fields
|
19
18
|
end
|
20
19
|
|
21
20
|
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
@@ -64,8 +63,7 @@ end
|
|
64
63
|
|
65
64
|
# A filter backend which handles ordering of the recordset.
|
66
65
|
class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
67
|
-
# Get a list of ordering fields for the current action.
|
68
|
-
# user wants to order by a virtual column.
|
66
|
+
# Get a list of ordering fields for the current action.
|
69
67
|
def _get_fields
|
70
68
|
return @controller.class.ordering_fields&.map(&:to_s) || @controller.get_fields
|
71
69
|
end
|
@@ -88,7 +86,8 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
88
86
|
column = field
|
89
87
|
direction = :asc
|
90
88
|
end
|
91
|
-
|
89
|
+
|
90
|
+
next if !column.in?(fields) && column.split(".").first.in?(fields)
|
92
91
|
|
93
92
|
ordering[column] = direction
|
94
93
|
end
|
@@ -113,15 +112,14 @@ end
|
|
113
112
|
|
114
113
|
# Multi-field text searching on models.
|
115
114
|
class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
116
|
-
# Get a list of search fields for the current action.
|
117
|
-
# common string-like columns by default.
|
115
|
+
# Get a list of search fields for the current action.
|
118
116
|
def _get_fields
|
119
117
|
if search_fields = @controller.class.search_fields
|
120
118
|
return search_fields&.map(&:to_s)
|
121
119
|
end
|
122
120
|
|
123
121
|
columns = @controller.class.get_model.column_names
|
124
|
-
return @controller.get_fields
|
122
|
+
return @controller.get_fields.select { |f|
|
125
123
|
f.in?(RESTFramework.config.search_columns) && f.in?(columns)
|
126
124
|
}
|
127
125
|
end
|
@@ -219,13 +219,18 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
219
219
|
includes = {}
|
220
220
|
methods = []
|
221
221
|
serializer_methods = {}
|
222
|
+
|
223
|
+
column_names = @model.column_names
|
224
|
+
reflections = @model.reflections
|
225
|
+
attachment_reflections = @model.attachment_reflections
|
226
|
+
|
222
227
|
fields.each do |f|
|
223
228
|
field_config = @controller.class.get_field_config(f)
|
224
229
|
next if field_config[:write_only]
|
225
230
|
|
226
|
-
if f.in?(
|
231
|
+
if f.in?(column_names)
|
227
232
|
columns << f
|
228
|
-
elsif ref =
|
233
|
+
elsif ref = reflections[f]
|
229
234
|
sub_columns = []
|
230
235
|
sub_methods = []
|
231
236
|
field_config[:sub_fields].each do |sf|
|
@@ -242,9 +247,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
242
247
|
# If we need to limit the number of serialized association records, then dynamically add a
|
243
248
|
# serializer method to do so.
|
244
249
|
if limit = self._get_associations_limit
|
245
|
-
|
246
|
-
|
247
|
-
self.define_singleton_method(method_name) do |record|
|
250
|
+
serializer_methods[f] = f
|
251
|
+
self.define_singleton_method(f) do |record|
|
248
252
|
next record.send(f).limit(limit).as_json(**sub_config)
|
249
253
|
end
|
250
254
|
else
|
@@ -253,8 +257,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
253
257
|
|
254
258
|
# If we need to include the association count, then add it here.
|
255
259
|
if @controller.class.native_serializer_include_associations_count
|
256
|
-
method_name = "
|
257
|
-
serializer_methods[method_name] =
|
260
|
+
method_name = "#{f}.count"
|
261
|
+
serializer_methods[method_name] = method_name
|
258
262
|
self.define_singleton_method(method_name) do |record|
|
259
263
|
next record.send(f).count
|
260
264
|
end
|
@@ -262,6 +266,24 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
262
266
|
else
|
263
267
|
includes[f] = sub_config
|
264
268
|
end
|
269
|
+
elsif ref = reflections["rich_text_#{f}"]
|
270
|
+
# ActionText Integration: Define rich text serializer method.
|
271
|
+
serializer_methods[f] = f
|
272
|
+
self.define_singleton_method(f) do |record|
|
273
|
+
next record.send(f).to_s
|
274
|
+
end
|
275
|
+
elsif ref = attachment_reflections[f]
|
276
|
+
# ActiveStorage Integration: Define attachment serializer method.
|
277
|
+
serializer_methods[f] = f
|
278
|
+
if ref.macro == :has_one_attached
|
279
|
+
self.define_singleton_method(f) do |record|
|
280
|
+
next record.send(f).attachment&.url
|
281
|
+
end
|
282
|
+
else
|
283
|
+
self.define_singleton_method(f) do |record|
|
284
|
+
next record.send(f).map(&:url)
|
285
|
+
end
|
286
|
+
end
|
265
287
|
elsif @model.method_defined?(f)
|
266
288
|
methods << f
|
267
289
|
end
|
@@ -272,9 +294,10 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
272
294
|
}
|
273
295
|
end
|
274
296
|
|
275
|
-
# Get the raw serializer config
|
276
|
-
#
|
277
|
-
|
297
|
+
# Get the raw serializer config, prior to any adjustments from the request.
|
298
|
+
#
|
299
|
+
# Use `deep_dup` on any class mutables (array, hash, etc) to avoid mutating class state.
|
300
|
+
def get_raw_serializer_config
|
278
301
|
# Return a locally defined serializer config if one is defined.
|
279
302
|
if local_config = self.get_local_native_serializer_config
|
280
303
|
return local_config.deep_dup
|
@@ -286,7 +309,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
286
309
|
end
|
287
310
|
|
288
311
|
# If the config wasn't determined, build a serializer config from controller fields.
|
289
|
-
if @model && fields = @controller&.get_fields
|
312
|
+
if @model && fields = @controller&.get_fields
|
290
313
|
return self._get_controller_serializer_config(fields.deep_dup)
|
291
314
|
end
|
292
315
|
|
@@ -296,7 +319,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
296
319
|
|
297
320
|
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
298
321
|
def get_serializer_config
|
299
|
-
return filter_from_request(self.
|
322
|
+
return filter_from_request(self.get_raw_serializer_config)
|
300
323
|
end
|
301
324
|
|
302
325
|
# Serialize a single record and merge results of `serializer_methods`.
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -180,7 +180,12 @@ module RESTFramework::Utils
|
|
180
180
|
return model.column_names.reject { |c|
|
181
181
|
c.in?(foreign_keys)
|
182
182
|
} + model.reflections.map { |association, ref|
|
183
|
-
#
|
183
|
+
# Ignore associations for which we have custom integrations.
|
184
|
+
if ref.class_name.in?(%w(ActiveStorage::Attachment ActiveStorage::Blob ActionText::RichText))
|
185
|
+
next nil
|
186
|
+
end
|
187
|
+
|
188
|
+
# Exclude user-specified associations.
|
184
189
|
if ref.class_name.in?(RESTFramework.config.exclude_association_classes)
|
185
190
|
next nil
|
186
191
|
end
|
@@ -192,7 +197,11 @@ module RESTFramework::Utils
|
|
192
197
|
end
|
193
198
|
|
194
199
|
next association
|
195
|
-
}.compact
|
200
|
+
}.compact + model.reflect_on_all_associations(:has_one).collect(&:name).select { |n|
|
201
|
+
n.to_s.start_with?("rich_text_")
|
202
|
+
}.map { |n|
|
203
|
+
n.to_s.delete_prefix("rich_text_")
|
204
|
+
} + model.attachment_reflections.keys
|
196
205
|
end
|
197
206
|
|
198
207
|
# Get the sub-fields that may be serialized and filtered/ordered for a reflection.
|
@@ -28,7 +28,10 @@ module RESTFramework
|
|
28
28
|
end
|
29
29
|
|
30
30
|
def self.stamp_version
|
31
|
-
|
31
|
+
# Only stamp the version if it's not unknown.
|
32
|
+
if RESTFramework::VERSION != "0.unknown"
|
33
|
+
File.write(VERSION_FILEPATH, RESTFramework::VERSION)
|
34
|
+
end
|
32
35
|
end
|
33
36
|
|
34
37
|
def self.unstamp_version
|
data/lib/rest_framework.rb
CHANGED
@@ -21,11 +21,7 @@ module RESTFramework
|
|
21
21
|
# Global configuration should be kept minimal, as controller-level configurations allows multiple
|
22
22
|
# APIs to be defined to behave differently.
|
23
23
|
class Config
|
24
|
-
DEFAULT_EXCLUDE_ASSOCIATION_CLASSES =
|
25
|
-
ActionText::RichText
|
26
|
-
ActiveStorage::Attachment
|
27
|
-
ActiveStorage::Blob
|
28
|
-
).freeze
|
24
|
+
DEFAULT_EXCLUDE_ASSOCIATION_CLASSES = [].freeze
|
29
25
|
DEFAULT_LABEL_FIELDS = %w(name label login title email username url).freeze
|
30
26
|
DEFAULT_SEARCH_COLUMNS = DEFAULT_LABEL_FIELDS + %w(description note).freeze
|
31
27
|
|
@@ -78,9 +74,7 @@ module RESTFramework
|
|
78
74
|
end
|
79
75
|
|
80
76
|
def self.features
|
81
|
-
return @features ||= {
|
82
|
-
html_forms: false,
|
83
|
-
}
|
77
|
+
return @features ||= {}
|
84
78
|
end
|
85
79
|
end
|
86
80
|
|