rest_framework 0.0.16 → 0.2.1
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 +16 -9
- data/lib/rest_framework.rb +3 -0
- data/lib/rest_framework/VERSION_STAMP +1 -1
- data/lib/rest_framework/controller_mixins.rb +4 -0
- data/lib/rest_framework/controller_mixins/base.rb +154 -166
- data/lib/rest_framework/controller_mixins/models.rb +215 -169
- data/lib/rest_framework/errors.rb +26 -0
- data/lib/rest_framework/filters.rb +53 -50
- data/lib/rest_framework/generators.rb +5 -0
- data/lib/rest_framework/generators/controller_generator.rb +26 -0
- data/lib/rest_framework/paginators.rb +76 -65
- data/lib/rest_framework/routers.rb +93 -44
- data/lib/rest_framework/serializers.rb +131 -93
- data/lib/rest_framework/version.rb +11 -9
- metadata +8 -5
@@ -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
|
@@ -1,65 +1,68 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
end
|
1
|
+
class RESTFramework::BaseFilter
|
2
|
+
def initialize(controller:)
|
3
|
+
@controller = controller
|
4
|
+
end
|
6
5
|
|
7
|
-
|
8
|
-
|
9
|
-
end
|
6
|
+
def get_filtered_data(data)
|
7
|
+
raise NotImplementedError
|
10
8
|
end
|
9
|
+
end
|
11
10
|
|
12
|
-
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
13
|
-
# controller class.
|
14
|
-
class ModelFilter < BaseFilter
|
15
|
-
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
16
|
-
def _get_filter_params
|
17
|
-
fields = @controller.class.filterset_fields || @controller.send(:get_fields)
|
18
|
-
return @controller.request.query_parameters.select { |p|
|
19
|
-
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
20
|
-
}.to_hash.symbolize_keys
|
21
|
-
end
|
22
11
|
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
12
|
+
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
13
|
+
# controller class.
|
14
|
+
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
15
|
+
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
16
|
+
def _get_filter_params
|
17
|
+
fields = @controller.class.filterset_fields&.map(&:to_s) || @controller.send(:get_fields)
|
18
|
+
return @controller.request.query_parameters.select { |p|
|
19
|
+
fields.include?(p)
|
20
|
+
}.to_h.symbolize_keys # convert from HashWithIndifferentAccess to Hash w/keys
|
21
|
+
end
|
28
22
|
|
29
|
-
|
23
|
+
# Filter data according to the request query parameters.
|
24
|
+
def get_filtered_data(data)
|
25
|
+
filter_params = self._get_filter_params
|
26
|
+
unless filter_params.blank?
|
27
|
+
return data.where(**filter_params)
|
30
28
|
end
|
29
|
+
|
30
|
+
return data
|
31
31
|
end
|
32
|
+
end
|
32
33
|
|
33
|
-
# A filter backend which handles ordering of the recordset.
|
34
|
-
class ModelOrderingFilter < BaseFilter
|
35
|
-
# Convert ordering string to an ordering configuration.
|
36
|
-
def _get_ordering
|
37
|
-
return nil unless @controller.class.ordering_query_param
|
38
34
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
else
|
45
|
-
[field.to_sym, :asc]
|
46
|
-
end
|
47
|
-
}.to_h
|
48
|
-
end
|
35
|
+
# A filter backend which handles ordering of the recordset.
|
36
|
+
class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
37
|
+
# Convert ordering string to an ordering configuration.
|
38
|
+
def _get_ordering
|
39
|
+
return nil unless @controller.class.ordering_query_param
|
49
40
|
|
50
|
-
|
41
|
+
order_string = @controller.params[@controller.class.ordering_query_param]
|
42
|
+
unless order_string.blank?
|
43
|
+
return order_string.split(',').map { |field|
|
44
|
+
if field[0] == '-'
|
45
|
+
[field[1..-1].to_sym, :desc]
|
46
|
+
else
|
47
|
+
[field.to_sym, :asc]
|
48
|
+
end
|
49
|
+
}.to_h
|
51
50
|
end
|
52
51
|
|
53
|
-
|
54
|
-
ordering = self._get_ordering
|
55
|
-
if ordering && !ordering.empty?
|
56
|
-
return data.order(_get_ordering)
|
57
|
-
end
|
58
|
-
return data
|
59
|
-
end
|
52
|
+
return nil
|
60
53
|
end
|
61
54
|
|
62
|
-
#
|
63
|
-
|
64
|
-
|
55
|
+
# Order data according to the request query parameters.
|
56
|
+
def get_filtered_data(data)
|
57
|
+
ordering = self._get_ordering
|
58
|
+
if ordering && !ordering.empty?
|
59
|
+
return data.order(_get_ordering)
|
60
|
+
end
|
61
|
+
return data
|
62
|
+
end
|
65
63
|
end
|
64
|
+
|
65
|
+
|
66
|
+
# TODO: implement searching within fields rather than exact match filtering (ModelFilter)
|
67
|
+
# class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
68
|
+
# end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'rails/generators'
|
2
|
+
|
3
|
+
|
4
|
+
class RESTFramework::Generators::ControllerGenerator < Rails::Generators::Base
|
5
|
+
desc <<~END
|
6
|
+
Description:
|
7
|
+
Stubs out a active_scaffolded controller. Pass the model name,
|
8
|
+
either CamelCased or under_scored.
|
9
|
+
The controller name is retrieved as a pluralized version of the model
|
10
|
+
name.
|
11
|
+
To create a controller within a module, specify the model name as a
|
12
|
+
path like 'parent_module/controller_name'.
|
13
|
+
This generates a controller class in app/controllers and invokes helper,
|
14
|
+
template engine and test framework generators.
|
15
|
+
Example:
|
16
|
+
`rails generate rest_framework:controller CreditCard`
|
17
|
+
Credit card controller with URLs like /credit_card/debit.
|
18
|
+
Controller: app/controllers/credit_cards_controller.rb
|
19
|
+
Functional Test: test/functional/credit_cards_controller_test.rb
|
20
|
+
Helper: app/helpers/credit_cards_helper.rb
|
21
|
+
END
|
22
|
+
|
23
|
+
def create_controller_file
|
24
|
+
create_file "app/controllers/some_controller.rb", "# Add initialization content here"
|
25
|
+
end
|
26
|
+
end
|
@@ -1,84 +1,95 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
class PageNumberPaginator
|
7
|
-
def initialize(data:, controller:, **kwargs)
|
8
|
-
@data = data
|
9
|
-
@controller = controller
|
10
|
-
@count = data.count
|
11
|
-
@page_size = self._page_size
|
12
|
-
|
13
|
-
@total_pages = @count / @page_size
|
14
|
-
@total_pages += 1 if @count % @page_size
|
15
|
-
end
|
1
|
+
class RESTFramework::BasePaginator
|
2
|
+
def initialize(data:, controller:, **kwargs)
|
3
|
+
@data = data
|
4
|
+
@controller = controller
|
5
|
+
end
|
16
6
|
|
17
|
-
|
18
|
-
|
7
|
+
# Get the page and return it so the caller can serialize it.
|
8
|
+
def get_page
|
9
|
+
raise NotImplementedError
|
10
|
+
end
|
19
11
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
end
|
26
|
-
end
|
12
|
+
# Wrap the serialized page with appropriate metadata.
|
13
|
+
def get_paginated_response(serialized_page)
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
end
|
27
17
|
|
28
|
-
# Otherwise, get from config.
|
29
|
-
if !page_size && @controller.class.page_size
|
30
|
-
page_size = @controller.class.page_size
|
31
|
-
end
|
32
18
|
|
33
|
-
|
34
|
-
|
19
|
+
# A simple paginator based on page numbers.
|
20
|
+
#
|
21
|
+
# Example: http://example.com/api/users/?page=3&page_size=50
|
22
|
+
class RESTFramework::PageNumberPaginator < RESTFramework::BasePaginator
|
23
|
+
def initialize(**kwargs)
|
24
|
+
super
|
25
|
+
@count = @data.count
|
26
|
+
@page_size = self._page_size
|
27
|
+
|
28
|
+
@total_pages = @count / @page_size
|
29
|
+
@total_pages += 1 if (@count % @page_size != 0)
|
30
|
+
end
|
31
|
+
|
32
|
+
def _page_size
|
33
|
+
page_size = nil
|
35
34
|
|
36
|
-
|
37
|
-
|
38
|
-
|
35
|
+
# Get from context, if allowed.
|
36
|
+
if @controller.class.page_size_query_param
|
37
|
+
if page_size = @controller.params[@controller.class.page_size_query_param].presence
|
38
|
+
page_size = page_size.to_i
|
39
39
|
end
|
40
|
+
end
|
40
41
|
|
41
|
-
|
42
|
-
|
42
|
+
# Otherwise, get from config.
|
43
|
+
if !page_size && @controller.class.page_size
|
44
|
+
page_size = @controller.class.page_size
|
43
45
|
end
|
44
46
|
|
45
|
-
|
46
|
-
|
47
|
+
# Ensure we don't exceed the max page size.
|
48
|
+
if @controller.class.max_page_size && page_size > @controller.class.max_page_size
|
49
|
+
page_size = @controller.class.max_page_size
|
47
50
|
end
|
48
51
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
52
|
+
# Ensure we return at least 1.
|
53
|
+
return page_size.zero? ? 1 : page_size
|
54
|
+
end
|
55
|
+
|
56
|
+
def _page_query_param
|
57
|
+
return @controller.class.page_query_param&.to_sym
|
58
|
+
end
|
59
|
+
|
60
|
+
# Get the page and return it so the caller can serialize it.
|
61
|
+
def get_page(page_number=nil)
|
62
|
+
# If page number isn't provided, infer from the params or use 1 as a fallback value.
|
63
|
+
if !page_number
|
64
|
+
page_number = @controller&.params&.[](self._page_query_param)
|
65
|
+
if page_number.blank?
|
66
|
+
page_number = 1
|
67
|
+
else
|
68
|
+
page_number = page_number.to_i
|
69
|
+
if page_number.zero?
|
54
70
|
page_number = 1
|
55
|
-
else
|
56
|
-
page_number = page_number.to_i
|
57
|
-
if page_number.zero?
|
58
|
-
page_number = 1
|
59
|
-
end
|
60
71
|
end
|
61
72
|
end
|
62
|
-
@page_number = page_number
|
63
|
-
|
64
|
-
# Get the data page and return it so the caller can serialize the data in the proper format.
|
65
|
-
page_index = @page_number - 1
|
66
|
-
return @data.limit(@page_size).offset(page_index * @page_size)
|
67
73
|
end
|
74
|
+
@page_number = page_number
|
68
75
|
|
69
|
-
#
|
70
|
-
|
71
|
-
|
72
|
-
count: @count,
|
73
|
-
page: @page_number,
|
74
|
-
total_pages: @total_pages,
|
75
|
-
results: serialized_page,
|
76
|
-
}
|
77
|
-
end
|
76
|
+
# Get the data page and return it so the caller can serialize the data in the proper format.
|
77
|
+
page_index = @page_number - 1
|
78
|
+
return @data.limit(@page_size).offset(page_index * @page_size)
|
78
79
|
end
|
79
80
|
|
80
|
-
# TODO:
|
81
|
-
|
82
|
-
|
83
|
-
|
81
|
+
# Wrap the serialized page with appropriate metadata. TODO: include links.
|
82
|
+
def get_paginated_response(serialized_page)
|
83
|
+
return {
|
84
|
+
count: @count,
|
85
|
+
page: @page_number,
|
86
|
+
total_pages: @total_pages,
|
87
|
+
results: serialized_page,
|
88
|
+
}
|
89
|
+
end
|
84
90
|
end
|
91
|
+
|
92
|
+
|
93
|
+
# TODO: implement this
|
94
|
+
# class RESTFramework::CountOffsetPaginator
|
95
|
+
# end
|
@@ -1,8 +1,49 @@
|
|
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.
|
7
|
+
protected def _parse_extra_actions(extra_actions)
|
8
|
+
return (extra_actions || {}).map do |k,v|
|
9
|
+
kwargs = {}
|
10
|
+
path = k
|
11
|
+
|
12
|
+
# Convert structure to path/methods/kwargs.
|
13
|
+
if v.is_a?(Hash) # allow kwargs
|
14
|
+
v = v.symbolize_keys
|
15
|
+
|
16
|
+
# Ensure methods is an array.
|
17
|
+
if v[:methods].is_a?(String) || v[:methods].is_a?(Symbol)
|
18
|
+
methods = [v.delete(:methods)]
|
19
|
+
else
|
20
|
+
methods = v.delete(:methods)
|
21
|
+
end
|
22
|
+
|
23
|
+
# Override path if it's provided.
|
24
|
+
if v.key?(:path)
|
25
|
+
path = v.delete(:path)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Set the action to be the action key unless it's already defined.
|
29
|
+
if !kwargs[:action]
|
30
|
+
kwargs[:action] = k
|
31
|
+
end
|
32
|
+
|
33
|
+
# Pass any further kwargs to the underlying Rails interface.
|
34
|
+
kwargs = kwargs.merge(v)
|
35
|
+
elsif v.is_a?(Symbol) || v.is_a?(String)
|
36
|
+
methods = [v]
|
37
|
+
else
|
38
|
+
methods = v
|
39
|
+
end
|
40
|
+
|
41
|
+
# Return a hash with keys: :path, :methods, :kwargs.
|
42
|
+
{path: path, methods: methods, kwargs: kwargs}
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Internal interface to get the controller class from the name and current scope.
|
6
47
|
protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
|
7
48
|
# get class name
|
8
49
|
name = name.to_s.camelize # camelize to leave plural names plural
|
@@ -36,20 +77,20 @@ module ActionDispatch::Routing
|
|
36
77
|
return controller
|
37
78
|
end
|
38
79
|
|
39
|
-
#
|
80
|
+
# Internal core implementation of the `rest_resource(s)` router, both singular and plural.
|
40
81
|
# @param default_singular [Boolean] the default plurality of the resource if the plurality is
|
41
82
|
# not otherwise defined by the controller
|
42
83
|
# @param name [Symbol] the resource name, from which path and controller are deduced by default
|
43
|
-
# @param skip_undefined [Boolean] whether we should skip routing undefined actions
|
84
|
+
# @param skip_undefined [Boolean] whether we should skip routing undefined resourceful actions
|
44
85
|
protected def _rest_resources(default_singular, name, skip_undefined: true, **kwargs, &block)
|
45
|
-
controller = kwargs
|
86
|
+
controller = kwargs.delete(:controller) || name
|
46
87
|
if controller.is_a?(Class)
|
47
88
|
controller_class = controller
|
48
89
|
else
|
49
90
|
controller_class = _get_controller_class(controller, pluralize: !default_singular)
|
50
91
|
end
|
51
92
|
|
52
|
-
#
|
93
|
+
# Set controller if it's not explicitly set.
|
53
94
|
kwargs[:controller] = name unless kwargs[:controller]
|
54
95
|
|
55
96
|
# determine plural/singular resource
|
@@ -70,25 +111,20 @@ module ActionDispatch::Routing
|
|
70
111
|
public_send(resource_method, name, except: skip, **kwargs) do
|
71
112
|
if controller_class.respond_to?(:extra_member_actions)
|
72
113
|
member do
|
73
|
-
actions = controller_class.extra_member_actions
|
74
|
-
actions
|
75
|
-
|
76
|
-
|
77
|
-
methods.each do |m|
|
78
|
-
public_send(m, action)
|
114
|
+
actions = self._parse_extra_actions(controller_class.extra_member_actions)
|
115
|
+
actions.each do |action_config|
|
116
|
+
action_config[:methods].each do |m|
|
117
|
+
public_send(m, action_config[:path], **action_config[:kwargs])
|
79
118
|
end
|
80
119
|
end
|
81
120
|
end
|
82
121
|
end
|
83
122
|
|
84
123
|
collection do
|
85
|
-
actions = controller_class.extra_actions
|
86
|
-
actions
|
87
|
-
|
88
|
-
|
89
|
-
methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
|
90
|
-
methods.each do |m|
|
91
|
-
public_send(m, action)
|
124
|
+
actions = self._parse_extra_actions(controller_class.extra_actions)
|
125
|
+
actions.each do |action_config|
|
126
|
+
action_config[:methods].each do |m|
|
127
|
+
public_send(m, action_config[:path], **action_config[:kwargs])
|
92
128
|
end
|
93
129
|
end
|
94
130
|
end
|
@@ -112,42 +148,55 @@ module ActionDispatch::Routing
|
|
112
148
|
end
|
113
149
|
|
114
150
|
# Route a controller without the default resourceful paths.
|
115
|
-
def rest_route(
|
116
|
-
controller = kwargs.delete(:controller) ||
|
117
|
-
|
151
|
+
def rest_route(name=nil, **kwargs, &block)
|
152
|
+
controller = kwargs.delete(:controller) || name
|
153
|
+
if controller.is_a?(Class)
|
154
|
+
controller_class = controller
|
155
|
+
else
|
156
|
+
controller_class = self._get_controller_class(controller, pluralize: false)
|
157
|
+
end
|
118
158
|
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
actions
|
123
|
-
actions =
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
159
|
+
# Set controller if it's not explicitly set.
|
160
|
+
kwargs[:controller] = name unless kwargs[:controller]
|
161
|
+
|
162
|
+
# Route actions using the resourceful router, but skip all builtin actions.
|
163
|
+
actions = self._parse_extra_actions(controller_class.extra_actions)
|
164
|
+
public_send(:resource, name, only: [], **kwargs) do
|
165
|
+
actions.each do |action_config|
|
166
|
+
action_config[:methods].each do |m|
|
167
|
+
public_send(m, action_config[:path], **action_config[:kwargs])
|
168
|
+
end
|
169
|
+
yield if block_given?
|
129
170
|
end
|
130
|
-
yield if block_given?
|
131
171
|
end
|
132
172
|
end
|
133
173
|
|
134
174
|
# Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
|
135
|
-
# @param
|
136
|
-
def rest_root(
|
137
|
-
#
|
175
|
+
# @param name [Symbol] the snake_case name of the controller
|
176
|
+
def rest_root(name=nil, **kwargs, &block)
|
177
|
+
# By default, use RootController#root.
|
138
178
|
root_action = kwargs.delete(:action) || :root
|
139
|
-
controller = kwargs.delete(:controller) ||
|
140
|
-
path = path.to_s
|
179
|
+
controller = kwargs.delete(:controller) || name || :root
|
141
180
|
|
142
|
-
#
|
143
|
-
get
|
181
|
+
# Route the root.
|
182
|
+
get name.to_s, controller: controller, action: root_action
|
144
183
|
|
145
|
-
#
|
184
|
+
# Route any additional actions.
|
146
185
|
controller_class = self._get_controller_class(controller, pluralize: false)
|
147
|
-
(controller_class.extra_actions
|
148
|
-
|
149
|
-
|
150
|
-
|
186
|
+
actions = self._parse_extra_actions(controller_class.extra_actions)
|
187
|
+
actions.each do |action_config|
|
188
|
+
# Add :action unless kwargs defines it.
|
189
|
+
unless action_config[:kwargs].key?(:action)
|
190
|
+
action_config[:kwargs][:action] = action_config[:path]
|
191
|
+
end
|
192
|
+
|
193
|
+
action_config[:methods].each do |m|
|
194
|
+
public_send(
|
195
|
+
m,
|
196
|
+
File.join(name.to_s, action_config[:path].to_s),
|
197
|
+
controller: controller,
|
198
|
+
**action_config[:kwargs],
|
199
|
+
)
|
151
200
|
end
|
152
201
|
yield if block_given?
|
153
202
|
end
|