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.
@@ -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
- module RESTFramework
2
- class BaseFilter
3
- def initialize(controller:)
4
- @controller = controller
5
- end
1
+ class RESTFramework::BaseFilter
2
+ def initialize(controller:)
3
+ @controller = controller
4
+ end
6
5
 
7
- def get_filtered_data(data)
8
- raise NotImplementedError
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
- def get_filtered_data(data)
24
- filter_params = self._get_filter_params
25
- unless filter_params.blank?
26
- return data.where(**filter_params)
27
- end
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
- return data
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
- order_string = @controller.params[@controller.class.ordering_query_param]
40
- unless order_string.blank?
41
- return order_string.split(',').map { |field|
42
- if field[0] == '-'
43
- [field[1..-1].to_sym, :desc]
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
- return nil
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
- def get_filtered_data(data)
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
- # TODO: implement searching within fields rather than exact match filtering (ModelFilter)
63
- # class ModelSearchFilter < BaseFilter
64
- # end
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,5 @@
1
+ module RESTFramework::Generators
2
+ end
3
+
4
+
5
+ require_relative 'generators/controller_generator'
@@ -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
- module RESTFramework
2
-
3
- # A simple paginator based on page numbers.
4
- #
5
- # Example: http://example.com/api/users/?page=3&page_size=50
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
- def _page_size
18
- page_size = nil
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
- # Get from context, if allowed.
21
- if @controller.class.page_size_query_param
22
- page_size = @controller.params[@controller.class.page_size_query_param].presence
23
- if page_size
24
- page_size = page_size.to_i
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
- # Fallback to a page size of 15.
34
- page_size = 15 unless page_size
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
- # Ensure we don't exceed the max page size.
37
- if @controller.class.max_page_size && page_size > @controller.class.max_page_size
38
- page_size = @controller.class.max_page_size
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
- # Ensure we return at least 1.
42
- return page_size.zero? ? 1 : page_size
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
- def _page_query_param
46
- return @controller.class.page_query_param&.to_sym
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
- def get_page(page_number=nil)
50
- # If page number isn't provided, infer from the params or use 1 as a fallback value.
51
- if !page_number
52
- page_number = @controller&.params&.[](self._page_query_param)
53
- if page_number.blank?
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
- # Wrap the serialized page with appripriate metadata.
70
- def get_paginated_response(serialized_page)
71
- return {
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: implement this
81
- # class CountOffsetPaginator
82
- # end
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
- # Private interface to get the controller class from the name and current scope.
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
- # Core implementation of the `rest_resource(s)` router, both singular and plural.
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[:controller] || name
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
- # set controller if it's not explicitly set
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 = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
75
- actions.each do |action, methods|
76
- methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
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 = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
87
- actions.reject! { |k,v| skip.include? k }
88
- actions.each do |action, methods|
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(path=nil, skip_undefined: true, **kwargs, &block)
116
- controller = kwargs.delete(:controller) || path
117
- path = path.to_s
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
- # route actions
120
- controller_class = self._get_controller_class(controller, pluralize: false)
121
- skip = controller_class.get_skip_actions(skip_undefined: skip_undefined)
122
- actions = controller_class.extra_actions || {}
123
- actions = actions.select { |k,v| controller_class.method_defined?(k) } if skip_undefined
124
- actions.reject! { |k,v| skip.include? k }
125
- actions.each do |action, methods|
126
- methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
127
- methods.each do |m|
128
- public_send(m, File.join(path, action.to_s), controller: controller, action: action)
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 label [Symbol] the snake_case name of the controller
136
- def rest_root(path=nil, **kwargs, &block)
137
- # by default, use RootController#root
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) || path || :root
140
- path = path.to_s
179
+ controller = kwargs.delete(:controller) || name || :root
141
180
 
142
- # route the root
143
- get path, controller: controller, action: root_action
181
+ # Route the root.
182
+ get name.to_s, controller: controller, action: root_action
144
183
 
145
- # route any additional actions
184
+ # Route any additional actions.
146
185
  controller_class = self._get_controller_class(controller, pluralize: false)
147
- (controller_class.extra_actions || {}).each do |action, methods|
148
- methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
149
- methods.each do |m|
150
- public_send(m, File.join(path, action.to_s), controller: controller, action: action)
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