rest_framework 0.1.3 → 0.2.4
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 +4 -4
- data/README.md +4 -2
- data/lib/rest_framework.rb +2 -0
- data/lib/rest_framework/VERSION_STAMP +1 -1
- data/lib/rest_framework/controller_mixins/base.rb +41 -30
- data/lib/rest_framework/controller_mixins/models.rb +78 -68
- data/lib/rest_framework/errors.rb +26 -0
- data/lib/rest_framework/filters.rb +3 -3
- data/lib/rest_framework/generators.rb +5 -0
- data/lib/rest_framework/generators/controller_generator.rb +67 -0
- data/lib/rest_framework/routers.rb +26 -37
- data/lib/rest_framework/serializers.rb +71 -24
- data/lib/rest_framework/version.rb +11 -9
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8d9ce4741f59e6f68bfa18bc2e6c11202c91fbe251eeaa5562f8f718e6e8afaa
|
4
|
+
data.tar.gz: 652ad42a8d0bad99e552af9117112df707e67d4c1e6dd2dd983e7925933fcab6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 26036468d40ccc77a203981ba442265ef82c7a991fee800ee3d7925d96d09a696f3db06930e8252c49cf63e86d2aee2fd121a735e5e06609585299ff984d6992
|
7
|
+
data.tar.gz: 3c53d8eed996e380ab3390acfde7a3cef262d38a1355d0016aa6d8129b8e329c99003c0181ec1602f5bb1ba8869355e6b1eb0ff366d8b814ff83f6bb3c9cbf4d
|
data/README.md
CHANGED
@@ -2,6 +2,8 @@
|
|
2
2
|
|
3
3
|
[](https://badge.fury.io/rb/rest_framework)
|
4
4
|
[](https://travis-ci.org/gregschmit/rails-rest-framework)
|
5
|
+
[](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
|
6
|
+
[](https://codeclimate.com/github/gregschmit/rails-rest-framework/maintainability)
|
5
7
|
|
6
8
|
A framework for DRY RESTful APIs in Ruby on Rails.
|
7
9
|
|
@@ -78,8 +80,8 @@ class Api::ReadOnlyMoviesController < ApiController
|
|
78
80
|
end
|
79
81
|
```
|
80
82
|
|
81
|
-
Note that you can also override
|
82
|
-
|
83
|
+
Note that you can also override the `get_recordset` instance method to override the API behavior
|
84
|
+
dynamically per-request.
|
83
85
|
|
84
86
|
### Routing
|
85
87
|
|
data/lib/rest_framework.rb
CHANGED
@@ -4,8 +4,10 @@ end
|
|
4
4
|
|
5
5
|
require_relative "rest_framework/controller_mixins"
|
6
6
|
require_relative "rest_framework/engine"
|
7
|
+
require_relative "rest_framework/errors"
|
7
8
|
require_relative "rest_framework/filters"
|
8
9
|
require_relative "rest_framework/paginators"
|
9
10
|
require_relative "rest_framework/routers"
|
10
11
|
require_relative "rest_framework/serializers"
|
11
12
|
require_relative "rest_framework/version"
|
13
|
+
require_relative "rest_framework/generators"
|
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.2.4
|
@@ -1,3 +1,4 @@
|
|
1
|
+
require_relative '../errors'
|
1
2
|
require_relative '../serializers'
|
2
3
|
|
3
4
|
|
@@ -33,6 +34,9 @@ module RESTFramework::BaseControllerMixin
|
|
33
34
|
|
34
35
|
# Add class attributes (with defaults) unless they already exist.
|
35
36
|
{
|
37
|
+
filter_pk_from_request_body: true,
|
38
|
+
exclude_body_fields: [:created_at, :created_by, :updated_at, :updated_by],
|
39
|
+
accept_generic_params_as_body_params: true,
|
36
40
|
extra_actions: nil,
|
37
41
|
extra_member_actions: nil,
|
38
42
|
filter_backends: nil,
|
@@ -42,14 +46,16 @@ module RESTFramework::BaseControllerMixin
|
|
42
46
|
page_size_query_param: 'page_size',
|
43
47
|
max_page_size: nil,
|
44
48
|
serializer_class: nil,
|
49
|
+
serialize_to_json: true,
|
50
|
+
serialize_to_xml: true,
|
45
51
|
singleton_controller: nil,
|
46
52
|
skip_actions: nil,
|
47
53
|
}.each do |a, default|
|
48
54
|
unless base.respond_to?(a)
|
49
55
|
base.class_attribute(a)
|
50
56
|
|
51
|
-
# Set default manually so we can still support Rails 4. Maybe later we can use the
|
52
|
-
#
|
57
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
58
|
+
# parameter on `class_attribute`.
|
53
59
|
base.send(:"#{a}=", default)
|
54
60
|
end
|
55
61
|
end
|
@@ -60,10 +66,10 @@ module RESTFramework::BaseControllerMixin
|
|
60
66
|
base.alias_method(:extra_collection_actions=, :extra_actions=)
|
61
67
|
end
|
62
68
|
|
63
|
-
#
|
69
|
+
# Skip csrf since this is an API.
|
64
70
|
base.skip_before_action(:verify_authenticity_token) rescue nil
|
65
71
|
|
66
|
-
#
|
72
|
+
# Handle some common exceptions.
|
67
73
|
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
68
74
|
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
69
75
|
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
@@ -118,7 +124,9 @@ module RESTFramework::BaseControllerMixin
|
|
118
124
|
begin
|
119
125
|
formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
|
120
126
|
rescue NameError
|
127
|
+
# :nocov:
|
121
128
|
formatter = ActionDispatch::Routing::ConsoleFormatter
|
129
|
+
# :nocov:
|
122
130
|
end
|
123
131
|
return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
|
124
132
|
formatter.new
|
@@ -129,39 +137,42 @@ module RESTFramework::BaseControllerMixin
|
|
129
137
|
|
130
138
|
# Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
|
131
139
|
# with support or passing custom `kwargs` to the underlying `render` calls.
|
132
|
-
def api_response(payload, html_kwargs: nil,
|
140
|
+
def api_response(payload, html_kwargs: nil, **kwargs)
|
133
141
|
html_kwargs ||= {}
|
134
|
-
json_kwargs
|
135
|
-
xml_kwargs
|
136
|
-
|
137
|
-
#
|
138
|
-
|
142
|
+
json_kwargs = kwargs.delete(:json_kwargs) || {}
|
143
|
+
xml_kwargs = kwargs.delete(:xml_kwargs) || {}
|
144
|
+
|
145
|
+
# Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
|
146
|
+
# when passing something like `User.find_by(id: some_id)` to `api_response`). The caller should
|
147
|
+
# actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
|
148
|
+
# framework to catch this error and return an appropriate error response.
|
149
|
+
if payload.nil?
|
150
|
+
raise RESTFramework::NilPassedToAPIResponseError
|
151
|
+
end
|
139
152
|
|
140
153
|
respond_to do |format|
|
141
|
-
if
|
142
|
-
format.json {head :no_content}
|
143
|
-
format.xml {head :no_content}
|
154
|
+
if payload == ''
|
155
|
+
format.json {head :no_content} if self.serialize_to_json
|
156
|
+
format.xml {head :no_content} if self.serialize_to_xml
|
144
157
|
else
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
render(xml: payload, layout: false, **xkwargs)
|
155
|
-
}
|
156
|
-
end
|
158
|
+
format.json {
|
159
|
+
jkwargs = kwargs.merge(json_kwargs)
|
160
|
+
render(json: payload, layout: false, **jkwargs)
|
161
|
+
} if self.serialize_to_json
|
162
|
+
format.xml {
|
163
|
+
xkwargs = kwargs.merge(xml_kwargs)
|
164
|
+
render(xml: payload, layout: false, **xkwargs)
|
165
|
+
} if self.serialize_to_xml
|
166
|
+
# TODO: possibly support more formats here if supported?
|
157
167
|
end
|
158
168
|
format.html {
|
159
169
|
@payload = payload
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
@
|
170
|
+
if payload == ''
|
171
|
+
@json_payload = '' if self.serialize_to_json
|
172
|
+
@xml_payload = '' if self.serialize_to_xml
|
173
|
+
else
|
174
|
+
@json_payload = payload.to_json if self.serialize_to_json
|
175
|
+
@xml_payload = payload.to_xml if self.serialize_to_xml
|
165
176
|
end
|
166
177
|
@template_logo_text ||= "Rails REST Framework"
|
167
178
|
@title ||= self.controller_name.camelize
|
@@ -16,17 +16,18 @@ module RESTFramework::BaseModelControllerMixin
|
|
16
16
|
model: nil,
|
17
17
|
recordset: nil,
|
18
18
|
|
19
|
-
# Attributes for create/update parameters.
|
20
|
-
allowed_parameters: nil,
|
21
|
-
allowed_action_parameters: nil,
|
22
|
-
|
23
19
|
# Attributes for configuring record fields.
|
24
20
|
fields: nil,
|
25
21
|
action_fields: nil,
|
26
22
|
|
23
|
+
# Attributes for create/update parameters.
|
24
|
+
allowed_parameters: nil,
|
25
|
+
allowed_action_parameters: nil,
|
26
|
+
|
27
27
|
# Attributes for the default native serializer.
|
28
28
|
native_serializer_config: nil,
|
29
|
-
|
29
|
+
native_serializer_singular_config: nil,
|
30
|
+
native_serializer_plural_config: nil,
|
30
31
|
|
31
32
|
# Attributes for default model filtering (and ordering).
|
32
33
|
filterset_fields: nil,
|
@@ -34,13 +35,13 @@ module RESTFramework::BaseModelControllerMixin
|
|
34
35
|
ordering_query_param: 'ordering',
|
35
36
|
|
36
37
|
# Other misc attributes.
|
37
|
-
disable_creation_from_recordset:
|
38
|
+
disable_creation_from_recordset: false, # Option to disable `recordset.create` behavior.
|
38
39
|
}.each do |a, default|
|
39
40
|
unless base.respond_to?(a)
|
40
41
|
base.class_attribute(a)
|
41
42
|
|
42
|
-
# Set default manually so we can still support Rails 4. Maybe later we can use the
|
43
|
-
#
|
43
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the default
|
44
|
+
# parameter on `class_attribute`.
|
44
45
|
base.send(:"#{a}=", default)
|
45
46
|
end
|
46
47
|
end
|
@@ -49,36 +50,34 @@ module RESTFramework::BaseModelControllerMixin
|
|
49
50
|
|
50
51
|
protected
|
51
52
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
action_serializer_config = self.class.native_serializer_action_config || {}
|
56
|
-
action = self.action_name.to_sym
|
57
|
-
|
58
|
-
# Handle case where :index action is not defined.
|
59
|
-
if action == :index && !action_serializer_config.key?(:index)
|
60
|
-
# Default is :show if `singleton_controller`, otherwise :list.
|
61
|
-
action = self.class.singleton_controller ? :show : :list
|
62
|
-
end
|
53
|
+
def _get_specific_action_config(action_config_key, generic_config_key)
|
54
|
+
action_config = self.class.send(action_config_key) || {}
|
55
|
+
action = self.action_name&.to_sym
|
63
56
|
|
64
|
-
|
65
|
-
|
57
|
+
# Index action should use :list serializer if :index is not provided.
|
58
|
+
action = :list if action == :index && !action_config.key?(:index)
|
66
59
|
|
67
|
-
|
68
|
-
# @return [RESTFramework::BaseSerializer]
|
69
|
-
def get_serializer_class
|
70
|
-
return self.class.serializer_class || RESTFramework::NativeSerializer
|
60
|
+
return (action_config[action] if action) || self.class.send(generic_config_key)
|
71
61
|
end
|
72
62
|
|
73
63
|
# Get a list of parameters allowed for the current action.
|
74
64
|
def get_allowed_parameters
|
75
|
-
|
76
|
-
|
65
|
+
return _get_specific_action_config(:allowed_action_parameters, :allowed_parameters)&.map(&:to_s)
|
66
|
+
end
|
77
67
|
|
78
|
-
|
79
|
-
|
68
|
+
# Get a list of fields for the current action.
|
69
|
+
def get_fields
|
70
|
+
return (
|
71
|
+
_get_specific_action_config(:action_fields, :fields)&.map(&:to_s) ||
|
72
|
+
self.get_model&.column_names ||
|
73
|
+
[]
|
74
|
+
)
|
75
|
+
end
|
80
76
|
|
81
|
-
|
77
|
+
# Helper to get the configured serializer class, or `NativeSerializer` as a default.
|
78
|
+
# @return [RESTFramework::BaseSerializer]
|
79
|
+
def get_serializer_class
|
80
|
+
return self.class.serializer_class || RESTFramework::NativeSerializer
|
82
81
|
end
|
83
82
|
|
84
83
|
# Get the list of filtering backends to use.
|
@@ -89,70 +88,81 @@ module RESTFramework::BaseModelControllerMixin
|
|
89
88
|
]
|
90
89
|
end
|
91
90
|
|
92
|
-
#
|
93
|
-
def
|
94
|
-
|
95
|
-
|
91
|
+
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
92
|
+
def get_body_params
|
93
|
+
return @_get_body_params ||= begin
|
94
|
+
fields = self.get_allowed_parameters || self.get_fields
|
95
|
+
|
96
|
+
# Filter the request body.
|
97
|
+
body_params = request.request_parameters.select { |p|
|
98
|
+
fields.include?(p)
|
99
|
+
}
|
100
|
+
|
101
|
+
# Add query params in place of missing body params, if configured.
|
102
|
+
if self.class.accept_generic_params_as_body_params
|
103
|
+
(fields - body_params.keys).each do |k|
|
104
|
+
if (value = params[k])
|
105
|
+
body_params[k] = value
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
96
109
|
|
97
|
-
|
98
|
-
|
110
|
+
# Filter primary key if configured.
|
111
|
+
if self.class.filter_pk_from_request_body
|
112
|
+
body_params.delete(self.get_model&.primary_key)
|
113
|
+
end
|
99
114
|
|
100
|
-
|
101
|
-
|
115
|
+
# Filter fields in exclude_body_fields.
|
116
|
+
(self.class.exclude_body_fields || []).each do |f|
|
117
|
+
body_params.delete(f.to_s)
|
118
|
+
end
|
102
119
|
|
103
|
-
|
104
|
-
|
105
|
-
fields = self.get_allowed_parameters || self.get_fields
|
106
|
-
return @get_body_params ||= (request.request_parameters.select { |p|
|
107
|
-
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
108
|
-
})
|
120
|
+
body_params
|
121
|
+
end
|
109
122
|
end
|
110
123
|
alias :get_create_params :get_body_params
|
111
124
|
alias :get_update_params :get_body_params
|
112
125
|
|
113
|
-
# Get a record by the primary key from the filtered recordset.
|
126
|
+
# Get a record by the primary key from the (non-filtered) recordset.
|
114
127
|
def get_record
|
115
|
-
records = self.get_filtered_data(self.get_recordset)
|
116
128
|
if pk = params[self.get_model.primary_key]
|
117
|
-
return
|
129
|
+
return self.get_recordset.find(pk)
|
118
130
|
end
|
119
131
|
return nil
|
120
132
|
end
|
121
133
|
|
122
|
-
#
|
123
|
-
def
|
134
|
+
# Get the model for this controller.
|
135
|
+
def get_model(from_get_recordset: false)
|
124
136
|
return @model if instance_variable_defined?(:@model) && @model
|
125
137
|
return (@model = self.class.model) if self.class.model
|
126
|
-
|
127
|
-
|
128
|
-
|
138
|
+
|
139
|
+
# Delegate to the recordset's model, if it's defined.
|
140
|
+
unless from_get_recordset # prevent infinite recursion
|
141
|
+
if (recordset = self.get_recordset)
|
142
|
+
return @model = recordset.klass
|
143
|
+
end
|
129
144
|
end
|
145
|
+
|
146
|
+
# Try to determine model from controller name.
|
130
147
|
begin
|
131
148
|
return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
|
132
149
|
rescue NameError
|
133
150
|
end
|
151
|
+
|
134
152
|
return nil
|
135
153
|
end
|
136
154
|
|
137
|
-
#
|
138
|
-
def
|
155
|
+
# Get the set of records this controller has access to.
|
156
|
+
def get_recordset
|
139
157
|
return @recordset if instance_variable_defined?(:@recordset) && @recordset
|
140
158
|
return (@recordset = self.class.recordset) if self.class.recordset
|
141
|
-
unless from_internal_get_model # prevent infinite recursion
|
142
|
-
model = self._get_model(from_internal_get_recordset: true)
|
143
|
-
return (@recordset = model.all) if model
|
144
|
-
end
|
145
|
-
return nil
|
146
|
-
end
|
147
159
|
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
160
|
+
# If there is a model, return that model's default scope (all records by default).
|
161
|
+
if (model = self.get_model(from_get_recordset: true))
|
162
|
+
return @recordset = model.all
|
163
|
+
end
|
152
164
|
|
153
|
-
|
154
|
-
def get_recordset
|
155
|
-
return _get_recordset
|
165
|
+
return nil
|
156
166
|
end
|
157
167
|
end
|
158
168
|
|
@@ -219,7 +229,7 @@ module RESTFramework::DestroyModelMixin
|
|
219
229
|
def destroy
|
220
230
|
@record = self.get_record
|
221
231
|
@record.destroy!
|
222
|
-
api_response(
|
232
|
+
api_response('')
|
223
233
|
end
|
224
234
|
end
|
225
235
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# Top-level class for all REST Framework errors.
|
2
|
+
class RESTFramework::Error < StandardError
|
3
|
+
end
|
4
|
+
|
5
|
+
class RESTFramework::NilPassedToAPIResponseError < RESTFramework::Error
|
6
|
+
def message
|
7
|
+
return <<~MSG.split("\n").join(' ')
|
8
|
+
Payload of `nil` was passed to `api_response`; this is unsupported. If you want a blank
|
9
|
+
response, pass `''` (an empty string) as the payload. If this was the result of a `find_by`
|
10
|
+
(or similar Active Record method) not finding a record, you should use the bang version (e.g.,
|
11
|
+
`find_by!`) to raise `ActiveRecord::RecordNotFound`, which the REST controller will catch and
|
12
|
+
return an appropriate error response.
|
13
|
+
MSG
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
class RESTFramework::UnserializableError < RESTFramework::Error
|
18
|
+
def initialize(object)
|
19
|
+
@object = object
|
20
|
+
return super
|
21
|
+
end
|
22
|
+
|
23
|
+
def message
|
24
|
+
return "Unable to serialize `#{@object.inspect}` (of type `#{@object.class}`)."
|
25
|
+
end
|
26
|
+
end
|
@@ -14,10 +14,10 @@ end
|
|
14
14
|
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
15
15
|
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
16
16
|
def _get_filter_params
|
17
|
-
fields = @controller.class.filterset_fields || @controller.send(:get_fields)
|
17
|
+
fields = @controller.class.filterset_fields&.map(&:to_s) || @controller.send(:get_fields)
|
18
18
|
return @controller.request.query_parameters.select { |p|
|
19
|
-
fields.include?(p
|
20
|
-
}.
|
19
|
+
fields.include?(p)
|
20
|
+
}.to_h.symbolize_keys # convert from HashWithIndifferentAccess to Hash w/keys
|
21
21
|
end
|
22
22
|
|
23
23
|
# Filter data according to the request query parameters.
|
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
|
4
|
+
# Some projects don't have the inflection "REST" as an acronym, so this is a helper class to prevent
|
5
|
+
# this generator from being namespaced under `r_e_s_t_framework`.
|
6
|
+
# :nocov:
|
7
|
+
class RESTFrameworkCustomGeneratorControllerNamespace < String
|
8
|
+
def camelize
|
9
|
+
return "RESTFramework"
|
10
|
+
end
|
11
|
+
end
|
12
|
+
# :nocov:
|
13
|
+
|
14
|
+
|
15
|
+
class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
|
16
|
+
PATH_REGEX = /^\/*([a-z0-9_\/]*[a-z0-9_])(?:[\.a-z\/]*)$/
|
17
|
+
|
18
|
+
desc <<~END
|
19
|
+
Description:
|
20
|
+
Generates a new REST Framework controller.
|
21
|
+
|
22
|
+
Specify the controller as a path, including the module, if needed, like:
|
23
|
+
'parent_module/controller_name'.
|
24
|
+
|
25
|
+
Example:
|
26
|
+
`rails generate rest_framework:controller user_api/groups`
|
27
|
+
|
28
|
+
Generates a controller at `app/controllers/user_api/groups_controller.rb` named
|
29
|
+
`UserApi::GroupsController`.
|
30
|
+
END
|
31
|
+
|
32
|
+
argument :path, type: :string
|
33
|
+
class_option(
|
34
|
+
:parent_class,
|
35
|
+
type: :string,
|
36
|
+
default: 'ApplicationController',
|
37
|
+
desc: "Inheritance parent",
|
38
|
+
)
|
39
|
+
class_option(
|
40
|
+
:include_base,
|
41
|
+
type: :boolean,
|
42
|
+
default: false,
|
43
|
+
desc: "Include `BaseControllerMixin`, not `ModelControllerMixin`",
|
44
|
+
)
|
45
|
+
|
46
|
+
# Some projects may not have the inflection "REST" as an acronym, which changes this generator to
|
47
|
+
# be namespaced in `r_e_s_t_framework`, which is weird.
|
48
|
+
def self.namespace
|
49
|
+
return RESTFrameworkCustomGeneratorControllerNamespace.new("rest_framework:controller")
|
50
|
+
end
|
51
|
+
|
52
|
+
def create_rest_controller_file
|
53
|
+
unless (path_match = PATH_REGEX.match(self.path))
|
54
|
+
raise StandardError.new("Path isn't correct.")
|
55
|
+
end
|
56
|
+
|
57
|
+
cleaned_path = path_match[1]
|
58
|
+
content = <<~END
|
59
|
+
class #{cleaned_path.camelize}Controller < #{options[:parent_class]}
|
60
|
+
include RESTFramework::#{
|
61
|
+
options[:include_base] ? "BaseControllerMixin" : "ModelControllerMixin"
|
62
|
+
}
|
63
|
+
end
|
64
|
+
END
|
65
|
+
create_file("app/controllers/#{path}_controller.rb", content)
|
66
|
+
end
|
67
|
+
end
|
@@ -1,11 +1,12 @@
|
|
1
1
|
require 'action_dispatch/routing/mapper'
|
2
2
|
|
3
|
+
|
3
4
|
module ActionDispatch::Routing
|
4
5
|
class Mapper
|
5
6
|
# Internal helper to take extra_actions hash and convert to a consistent format.
|
6
7
|
protected def _parse_extra_actions(extra_actions)
|
7
8
|
return (extra_actions || {}).map do |k,v|
|
8
|
-
kwargs = {}
|
9
|
+
kwargs = {action: k}
|
9
10
|
path = k
|
10
11
|
|
11
12
|
# Convert structure to path/methods/kwargs.
|
@@ -71,6 +72,16 @@ module ActionDispatch::Routing
|
|
71
72
|
return controller
|
72
73
|
end
|
73
74
|
|
75
|
+
# Interal interface for routing extra actions.
|
76
|
+
protected def _route_extra_actions(actions, &block)
|
77
|
+
actions.each do |action_config|
|
78
|
+
action_config[:methods].each do |m|
|
79
|
+
public_send(m, action_config[:path], **action_config[:kwargs])
|
80
|
+
end
|
81
|
+
yield if block_given?
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
74
85
|
# Internal core implementation of the `rest_resource(s)` router, both singular and plural.
|
75
86
|
# @param default_singular [Boolean] the default plurality of the resource if the plurality is
|
76
87
|
# not otherwise defined by the controller
|
@@ -99,28 +110,20 @@ module ActionDispatch::Routing
|
|
99
110
|
end
|
100
111
|
resource_method = singular ? :resource : :resources
|
101
112
|
|
102
|
-
#
|
113
|
+
# Call either `resource` or `resources`, passing appropriate modifiers.
|
103
114
|
skip_undefined = kwargs.delete(:skip_undefined) || true
|
104
115
|
skip = controller_class.get_skip_actions(skip_undefined: skip_undefined)
|
105
116
|
public_send(resource_method, name, except: skip, **kwargs) do
|
106
117
|
if controller_class.respond_to?(:extra_member_actions)
|
107
118
|
member do
|
108
119
|
actions = self._parse_extra_actions(controller_class.extra_member_actions)
|
109
|
-
actions
|
110
|
-
action_config[:methods].each do |m|
|
111
|
-
public_send(m, action_config[:path], **action_config[:kwargs])
|
112
|
-
end
|
113
|
-
end
|
120
|
+
_route_extra_actions(actions)
|
114
121
|
end
|
115
122
|
end
|
116
123
|
|
117
124
|
collection do
|
118
125
|
actions = self._parse_extra_actions(controller_class.extra_actions)
|
119
|
-
actions
|
120
|
-
action_config[:methods].each do |m|
|
121
|
-
public_send(m, action_config[:path], **action_config[:kwargs])
|
122
|
-
end
|
123
|
-
end
|
126
|
+
_route_extra_actions(actions)
|
124
127
|
end
|
125
128
|
|
126
129
|
yield if block_given?
|
@@ -144,6 +147,7 @@ module ActionDispatch::Routing
|
|
144
147
|
# Route a controller without the default resourceful paths.
|
145
148
|
def rest_route(name=nil, **kwargs, &block)
|
146
149
|
controller = kwargs.delete(:controller) || name
|
150
|
+
route_root_to = kwargs.delete(:route_root_to)
|
147
151
|
if controller.is_a?(Class)
|
148
152
|
controller_class = controller
|
149
153
|
else
|
@@ -156,42 +160,27 @@ module ActionDispatch::Routing
|
|
156
160
|
# Route actions using the resourceful router, but skip all builtin actions.
|
157
161
|
actions = self._parse_extra_actions(controller_class.extra_actions)
|
158
162
|
public_send(:resource, name, only: [], **kwargs) do
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
end
|
163
|
-
yield if block_given?
|
163
|
+
# Route a root for this resource.
|
164
|
+
if route_root_to
|
165
|
+
get '', action: route_root_to
|
164
166
|
end
|
167
|
+
|
168
|
+
_route_extra_actions(actions, &block)
|
165
169
|
end
|
166
170
|
end
|
167
171
|
|
168
172
|
# Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
|
169
|
-
# @param name [Symbol] the snake_case name of the controller
|
170
173
|
def rest_root(name=nil, **kwargs, &block)
|
171
174
|
# By default, use RootController#root.
|
172
175
|
root_action = kwargs.delete(:action) || :root
|
173
176
|
controller = kwargs.delete(:controller) || name || :root
|
174
177
|
|
175
|
-
#
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
controller_class = self._get_controller_class(controller, pluralize: false)
|
180
|
-
actions = self._parse_extra_actions(controller_class.extra_actions)
|
181
|
-
actions.each do |action_config|
|
182
|
-
# Add :action unless kwargs defines it.
|
183
|
-
unless action_config[:kwargs].key?(:action)
|
184
|
-
action_config[:kwargs][:action] = action_config[:path]
|
185
|
-
end
|
178
|
+
# Remove path if name is nil (routing to the root of current namespace).
|
179
|
+
unless name
|
180
|
+
kwargs[:path] = ''
|
181
|
+
end
|
186
182
|
|
187
|
-
|
188
|
-
public_send(
|
189
|
-
m,
|
190
|
-
File.join(name.to_s, action_config[:path].to_s),
|
191
|
-
controller: controller,
|
192
|
-
**action_config[:kwargs],
|
193
|
-
)
|
194
|
-
end
|
183
|
+
return rest_route(controller, route_root_to: root_action, **kwargs) do
|
195
184
|
yield if block_given?
|
196
185
|
end
|
197
186
|
end
|
@@ -1,16 +1,17 @@
|
|
1
1
|
class RESTFramework::BaseSerializer
|
2
|
-
attr_reader :errors
|
3
|
-
|
4
2
|
def initialize(object: nil, controller: nil, **kwargs)
|
5
3
|
@object = object
|
6
4
|
@controller = controller
|
7
5
|
end
|
6
|
+
|
7
|
+
def serialize
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
8
10
|
end
|
9
11
|
|
10
12
|
|
11
|
-
# This serializer uses `.
|
12
|
-
#
|
13
|
-
# an array or a hash).
|
13
|
+
# This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
|
14
|
+
# top-level being either an array or a hash).
|
14
15
|
class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
15
16
|
class_attribute :config
|
16
17
|
class_attribute :singular_config
|
@@ -21,12 +22,19 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
21
22
|
super(**kwargs)
|
22
23
|
|
23
24
|
if many.nil?
|
24
|
-
|
25
|
+
# Determine if we are dealing with many objects or just one.
|
26
|
+
@many = @object.is_a?(Enumerable)
|
25
27
|
else
|
26
28
|
@many = many
|
27
29
|
end
|
28
30
|
|
29
|
-
|
31
|
+
# Determine model either explicitly, or by inspecting @object or @controller.
|
32
|
+
@model = model
|
33
|
+
@model ||= @object.class if @object.is_a?(ActiveRecord::Base)
|
34
|
+
@model ||= @object[0].class if (
|
35
|
+
@many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
|
36
|
+
)
|
37
|
+
@model ||= @controller.send(:get_model) if @controller
|
30
38
|
end
|
31
39
|
|
32
40
|
# Get controller action, if possible.
|
@@ -34,8 +42,8 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
34
42
|
return @controller&.action_name&.to_sym
|
35
43
|
end
|
36
44
|
|
37
|
-
# Get a locally defined configuration, if one is defined.
|
38
|
-
def
|
45
|
+
# Get a locally defined native serializer configuration, if one is defined.
|
46
|
+
def get_local_native_serializer_config
|
39
47
|
action = self.get_action
|
40
48
|
|
41
49
|
if action && self.action_config
|
@@ -45,43 +53,70 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
45
53
|
return self.action_config[action] if self.action_config[action]
|
46
54
|
end
|
47
55
|
|
48
|
-
# No action_config, so try singular/plural config.
|
49
|
-
return self.plural_config if @many && self.plural_config
|
50
|
-
return self.singular_config if
|
56
|
+
# No action_config, so try singular/plural config if explicitly instructed to via @many.
|
57
|
+
return self.plural_config if @many == true && self.plural_config
|
58
|
+
return self.singular_config if @many == false && self.singular_config
|
51
59
|
|
52
|
-
# Lastly, try returning the default config.
|
53
|
-
return self.config
|
60
|
+
# Lastly, try returning the default config, or singular/plural config in that order.
|
61
|
+
return self.config || self.singular_config || self.plural_config
|
54
62
|
end
|
55
63
|
|
56
|
-
#
|
64
|
+
# Helper to get a native serializer configuration from the controller.
|
65
|
+
def get_controller_native_serializer_config
|
66
|
+
return nil unless @controller
|
67
|
+
|
68
|
+
if @many == true
|
69
|
+
controller_serializer = @controller.try(:native_serializer_plural_config)
|
70
|
+
elsif @many == false
|
71
|
+
controller_serializer = @controller.try(:native_serializer_singular_config)
|
72
|
+
end
|
73
|
+
|
74
|
+
return controller_serializer || @controller.try(:native_serializer_config)
|
75
|
+
end
|
76
|
+
|
77
|
+
# Get a configuration passable to `serializable_hash` for the object.
|
57
78
|
def get_serializer_config
|
58
79
|
# Return a locally defined serializer config if one is defined.
|
59
|
-
if local_config = self.
|
80
|
+
if local_config = self.get_local_native_serializer_config
|
60
81
|
return local_config
|
61
82
|
end
|
62
83
|
|
63
|
-
# Return a serializer config if one is defined.
|
64
|
-
if serializer_config =
|
84
|
+
# Return a serializer config if one is defined on the controller.
|
85
|
+
if serializer_config = get_controller_native_serializer_config
|
65
86
|
return serializer_config
|
66
87
|
end
|
67
88
|
|
68
89
|
# If the config wasn't determined, build a serializer config from model fields.
|
69
|
-
fields = @controller.
|
70
|
-
|
71
|
-
|
90
|
+
fields = @controller.send(:get_fields) if @controller
|
91
|
+
if fields
|
92
|
+
if @model
|
93
|
+
columns, methods = fields.partition { |f| f.in?(@model.column_names) }
|
94
|
+
else
|
95
|
+
columns = fields
|
96
|
+
methods = []
|
97
|
+
end
|
98
|
+
|
72
99
|
return {only: columns, methods: methods}
|
73
100
|
end
|
74
101
|
|
102
|
+
# By default, pass an empty configuration, allowing the serialization of all columns.
|
75
103
|
return {}
|
76
104
|
end
|
77
105
|
|
78
106
|
# Convert the object (record or recordset) to Ruby primitives.
|
79
107
|
def serialize
|
80
108
|
if @object
|
81
|
-
|
82
|
-
|
109
|
+
begin
|
110
|
+
if @object.is_a?(Enumerable)
|
111
|
+
return @object.map { |r| r.serializable_hash(self.get_serializer_config) }
|
112
|
+
end
|
113
|
+
return @object.serializable_hash(self.get_serializer_config)
|
114
|
+
rescue NoMethodError
|
115
|
+
end
|
83
116
|
end
|
84
|
-
|
117
|
+
|
118
|
+
# Raise an error if we cannot serialize the object.
|
119
|
+
raise RESTFramework::UnserializableError.new(@object)
|
85
120
|
end
|
86
121
|
|
87
122
|
# Allow a serializer instance to be used as a hash directly in a nested serializer config.
|
@@ -113,6 +148,18 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
113
148
|
end
|
114
149
|
end
|
115
150
|
|
151
|
+
|
152
|
+
# :nocov:
|
116
153
|
# Alias NativeModelSerializer -> NativeSerializer.
|
117
154
|
class RESTFramework::NativeModelSerializer < RESTFramework::NativeSerializer
|
155
|
+
def initialize(**kwargs)
|
156
|
+
super
|
157
|
+
ActiveSupport::Deprecation.warn(
|
158
|
+
<<~MSG.split("\n").join(' ')
|
159
|
+
RESTFramework::NativeModelSerializer is deprecated and will be removed in future versions of
|
160
|
+
REST Framework; you should use RESTFramework::NativeSerializer instead.
|
161
|
+
MSG
|
162
|
+
)
|
163
|
+
end
|
118
164
|
end
|
165
|
+
# :nocov:
|
@@ -2,19 +2,21 @@ module RESTFramework
|
|
2
2
|
module Version
|
3
3
|
@_version = nil
|
4
4
|
|
5
|
-
def self.get_version
|
5
|
+
def self.get_version(skip_git: false)
|
6
6
|
# Return cached @_version, if available.
|
7
7
|
return @_version if @_version
|
8
8
|
|
9
9
|
# First, attempt to get the version from git.
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
10
|
+
unless skip_git
|
11
|
+
begin
|
12
|
+
version = `git describe 2>/dev/null`.strip
|
13
|
+
raise "blank version" if version.nil? || version.match(/^\w*$/)
|
14
|
+
# Check for local changes.
|
15
|
+
changes = `git status --porcelain 2>/dev/null`
|
16
|
+
version << '.localchanges' if changes.strip.length > 0
|
17
|
+
return version
|
18
|
+
rescue
|
19
|
+
end
|
18
20
|
end
|
19
21
|
|
20
22
|
# Git failed, so try to find a VERSION_STAMP.
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rest_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gregory N. Schmit
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2021-03-
|
11
|
+
date: 2021-03-29 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -43,7 +43,10 @@ files:
|
|
43
43
|
- lib/rest_framework/controller_mixins/base.rb
|
44
44
|
- lib/rest_framework/controller_mixins/models.rb
|
45
45
|
- lib/rest_framework/engine.rb
|
46
|
+
- lib/rest_framework/errors.rb
|
46
47
|
- lib/rest_framework/filters.rb
|
48
|
+
- lib/rest_framework/generators.rb
|
49
|
+
- lib/rest_framework/generators/controller_generator.rb
|
47
50
|
- lib/rest_framework/paginators.rb
|
48
51
|
- lib/rest_framework/routers.rb
|
49
52
|
- lib/rest_framework/serializers.rb
|