rest_framework 0.1.3 → 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
|
4
4
|
[![Build Status](https://travis-ci.org/gregschmit/rails-rest-framework.svg?branch=master)](https://travis-ci.org/gregschmit/rails-rest-framework)
|
5
|
+
[![Coverage Status](https://coveralls.io/repos/github/gregschmit/rails-rest-framework/badge.svg?branch=master)](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
|
6
|
+
[![Maintainability](https://api.codeclimate.com/v1/badges/ba5df7706cb544d78555/maintainability)](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
|