rest_framework 0.0.13 → 0.1.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,68 @@
1
+ class RESTFramework::BaseFilter
2
+ def initialize(controller:)
3
+ @controller = controller
4
+ end
5
+
6
+ def get_filtered_data(data)
7
+ raise NotImplementedError
8
+ end
9
+ end
10
+
11
+
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 || @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
+
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)
28
+ end
29
+
30
+ return data
31
+ end
32
+ end
33
+
34
+
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
40
+
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
50
+ end
51
+
52
+ return nil
53
+ end
54
+
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
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,95 @@
1
+ class RESTFramework::BasePaginator
2
+ def initialize(data:, controller:, **kwargs)
3
+ @data = data
4
+ @controller = controller
5
+ end
6
+
7
+ # Get the page and return it so the caller can serialize it.
8
+ def get_page
9
+ raise NotImplementedError
10
+ end
11
+
12
+ # Wrap the serialized page with appropriate metadata.
13
+ def get_paginated_response(serialized_page)
14
+ raise NotImplementedError
15
+ end
16
+ end
17
+
18
+
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
34
+
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
+ end
40
+ end
41
+
42
+ # Otherwise, get from config.
43
+ if !page_size && @controller.class.page_size
44
+ page_size = @controller.class.page_size
45
+ end
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
50
+ end
51
+
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?
70
+ page_number = 1
71
+ end
72
+ end
73
+ end
74
+ @page_number = page_number
75
+
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)
79
+ end
80
+
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
90
+ end
91
+
92
+
93
+ # TODO: implement this
94
+ # class RESTFramework::CountOffsetPaginator
95
+ # end
@@ -2,7 +2,42 @@ require 'action_dispatch/routing/mapper'
2
2
 
3
3
  module ActionDispatch::Routing
4
4
  class Mapper
5
- # Private interface to get the controller class from the name and current scope.
5
+ # Internal helper to take extra_actions hash and convert to a consistent format.
6
+ protected def _parse_extra_actions(extra_actions)
7
+ return (extra_actions || {}).map do |k,v|
8
+ kwargs = {}
9
+ path = k
10
+
11
+ # Convert structure to path/methods/kwargs.
12
+ if v.is_a?(Hash) # allow kwargs
13
+ v = v.symbolize_keys
14
+
15
+ # Ensure methods is an array.
16
+ if v[:methods].is_a?(String) || v[:methods].is_a?(Symbol)
17
+ methods = [v.delete(:methods)]
18
+ else
19
+ methods = v.delete(:methods)
20
+ end
21
+
22
+ # Override path if it's provided.
23
+ if v.key?(:path)
24
+ path = v.delete(:path)
25
+ end
26
+
27
+ # Pass any further kwargs to the underlying Rails interface.
28
+ kwargs = kwargs.merge(v)
29
+ elsif v.is_a?(Symbol) || v.is_a?(String)
30
+ methods = [v]
31
+ else
32
+ methods = v
33
+ end
34
+
35
+ # Return a hash with keys: :path, :methods, :kwargs.
36
+ {path: path, methods: methods, kwargs: kwargs}
37
+ end
38
+ end
39
+
40
+ # Internal interface to get the controller class from the name and current scope.
6
41
  protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
7
42
  # get class name
8
43
  name = name.to_s.camelize # camelize to leave plural names plural
@@ -36,20 +71,20 @@ module ActionDispatch::Routing
36
71
  return controller
37
72
  end
38
73
 
39
- # Core implementation of the `rest_resource(s)` router, both singular and plural.
74
+ # Internal core implementation of the `rest_resource(s)` router, both singular and plural.
40
75
  # @param default_singular [Boolean] the default plurality of the resource if the plurality is
41
76
  # not otherwise defined by the controller
42
77
  # @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
78
+ # @param skip_undefined [Boolean] whether we should skip routing undefined resourceful actions
44
79
  protected def _rest_resources(default_singular, name, skip_undefined: true, **kwargs, &block)
45
- controller = kwargs[:controller] || name
80
+ controller = kwargs.delete(:controller) || name
46
81
  if controller.is_a?(Class)
47
82
  controller_class = controller
48
83
  else
49
84
  controller_class = _get_controller_class(controller, pluralize: !default_singular)
50
85
  end
51
86
 
52
- # set controller if it's not explicitly set
87
+ # Set controller if it's not explicitly set.
53
88
  kwargs[:controller] = name unless kwargs[:controller]
54
89
 
55
90
  # determine plural/singular resource
@@ -70,25 +105,20 @@ module ActionDispatch::Routing
70
105
  public_send(resource_method, name, except: skip, **kwargs) do
71
106
  if controller_class.respond_to?(:extra_member_actions)
72
107
  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)
108
+ actions = self._parse_extra_actions(controller_class.extra_member_actions)
109
+ actions.each do |action_config|
110
+ action_config[:methods].each do |m|
111
+ public_send(m, action_config[:path], **action_config[:kwargs])
79
112
  end
80
113
  end
81
114
  end
82
115
  end
83
116
 
84
117
  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)
118
+ actions = self._parse_extra_actions(controller_class.extra_actions)
119
+ actions.each do |action_config|
120
+ action_config[:methods].each do |m|
121
+ public_send(m, action_config[:path], **action_config[:kwargs])
92
122
  end
93
123
  end
94
124
  end
@@ -112,42 +142,55 @@ module ActionDispatch::Routing
112
142
  end
113
143
 
114
144
  # 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
145
+ def rest_route(name=nil, **kwargs, &block)
146
+ controller = kwargs.delete(:controller) || name
147
+ if controller.is_a?(Class)
148
+ controller_class = controller
149
+ else
150
+ controller_class = self._get_controller_class(controller, pluralize: false)
151
+ end
118
152
 
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)
153
+ # Set controller if it's not explicitly set.
154
+ kwargs[:controller] = name unless kwargs[:controller]
155
+
156
+ # Route actions using the resourceful router, but skip all builtin actions.
157
+ actions = self._parse_extra_actions(controller_class.extra_actions)
158
+ public_send(:resource, name, only: [], **kwargs) do
159
+ actions.each do |action_config|
160
+ action_config[:methods].each do |m|
161
+ public_send(m, action_config[:path], **action_config[:kwargs])
162
+ end
163
+ yield if block_given?
129
164
  end
130
- yield if block_given?
131
165
  end
132
166
  end
133
167
 
134
168
  # 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
169
+ # @param name [Symbol] the snake_case name of the controller
170
+ def rest_root(name=nil, **kwargs, &block)
171
+ # By default, use RootController#root.
138
172
  root_action = kwargs.delete(:action) || :root
139
- controller = kwargs.delete(:controller) || path || :root
140
- path = path.to_s
173
+ controller = kwargs.delete(:controller) || name || :root
141
174
 
142
- # route the root
143
- get path, controller: controller, action: root_action
175
+ # Route the root.
176
+ get name.to_s, controller: controller, action: root_action
144
177
 
145
- # route any additional actions
178
+ # Route any additional actions.
146
179
  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)
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
186
+
187
+ action_config[:methods].each do |m|
188
+ public_send(
189
+ m,
190
+ File.join(name.to_s, action_config[:path].to_s),
191
+ controller: controller,
192
+ **action_config[:kwargs],
193
+ )
151
194
  end
152
195
  yield if block_given?
153
196
  end
@@ -1,108 +1,114 @@
1
- module RESTFramework
2
- class BaseSerializer
3
- attr_reader :errors
4
-
5
- def initialize(object: nil, data: nil, controller: nil, **kwargs)
6
- @object = object
7
- @data = data
8
- @controller = controller
9
- end
1
+ class RESTFramework::BaseSerializer
2
+ attr_reader :errors
3
+
4
+ def initialize(object: nil, controller: nil, **kwargs)
5
+ @object = object
6
+ @controller = controller
10
7
  end
8
+ end
11
9
 
12
- # This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is a Rails
13
- # method which converts objects to Ruby primitives (with the top-level being either an array or a
14
- # hash).
15
- class NativeModelSerializer < BaseSerializer
16
- class_attribute :config
17
- class_attribute :singular_config
18
- class_attribute :plural_config
19
- class_attribute :action_config
20
-
21
- def initialize(model: nil, many: nil, **kwargs)
22
- super(**kwargs)
23
- @many = many
24
- @model = model || (@controller ? @controller.send(:get_model) : nil)
25
- end
26
10
 
27
- # Get controller action, if possible.
28
- def get_action
29
- return @controller&.action_name&.to_sym
11
+ # This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is an
12
+ # `ActiveModel` method which converts objects to Ruby primitives (with the top-level being either
13
+ # an array or a hash).
14
+ class RESTFramework::NativeModelSerializer < RESTFramework::BaseSerializer
15
+ class_attribute :config
16
+ class_attribute :singular_config
17
+ class_attribute :plural_config
18
+ class_attribute :action_config
19
+
20
+ def initialize(many: nil, model: nil, **kwargs)
21
+ super(**kwargs)
22
+
23
+ if many.nil?
24
+ @many = @object.respond_to?(:count) ? @object.count : nil
25
+ else
26
+ @many = many
30
27
  end
31
28
 
32
- # Get a locally defined configuration, if one is defined.
33
- def get_local_serializer_config
34
- action = self.get_action
29
+ @model = model || (@controller ? @controller.send(:get_model) : nil)
30
+ end
35
31
 
36
- if action && self.action_config
37
- # index action should use :list serializer config if :index is not provided
38
- action = :list if action == :index && !self.action_config.key?(:index)
32
+ # Get controller action, if possible.
33
+ def get_action
34
+ return @controller&.action_name&.to_sym
35
+ end
39
36
 
40
- return self.action_config[action] if self.action_config[action]
41
- end
37
+ # Get a locally defined configuration, if one is defined.
38
+ def get_local_serializer_config
39
+ action = self.get_action
42
40
 
43
- # no action_config, so try singular/plural config
44
- return self.plural_config if @many && self.plural_config
45
- return self.singular_config if !@many && self.singular_config
41
+ if action && self.action_config
42
+ # Index action should use :list serializer config if :index is not provided.
43
+ action = :list if action == :index && !self.action_config.key?(:index)
46
44
 
47
- # lastly, try the default config
48
- return self.config
45
+ return self.action_config[action] if self.action_config[action]
49
46
  end
50
47
 
51
- # Get a configuration passable to `as_json` for the model.
52
- def get_serializer_config
53
- # return a locally defined serializer config if one is defined
54
- local_config = self.get_local_serializer_config
55
- return local_config if local_config
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 !@many && self.singular_config
56
51
 
57
- # return a serializer config if one is defined
58
- serializer_config = @controller.send(:get_native_serializer_config)
59
- return serializer_config if serializer_config
52
+ # Lastly, try returning the default config.
53
+ return self.config
54
+ end
60
55
 
61
- # otherwise, build a serializer config from fields
62
- fields = @controller.send(:get_fields) if @controller
63
- unless fields.blank?
64
- columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
65
- return {only: columns, methods: methods}
66
- end
56
+ # Get a configuration passable to `as_json` for the object.
57
+ def get_serializer_config
58
+ # Return a locally defined serializer config if one is defined.
59
+ if local_config = self.get_local_serializer_config
60
+ return local_config
61
+ end
67
62
 
68
- return {}
63
+ # Return a serializer config if one is defined.
64
+ if serializer_config = @controller.send(:get_native_serializer_config)
65
+ return serializer_config
69
66
  end
70
67
 
71
- # Convert the object(s) to Ruby primitives.
72
- def serialize
73
- if @object
74
- @many = @object.respond_to?(:each) if @many.nil?
75
- return @object.as_json(self.get_serializer_config)
76
- end
77
- return nil
68
+ # If the config wasn't determined, build a serializer config from model fields.
69
+ fields = @controller.try(:get_fields) if @controller
70
+ unless fields.blank?
71
+ columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
72
+ return {only: columns, methods: methods}
73
+ end
74
+
75
+ return {}
76
+ end
77
+
78
+ # Convert the object (record or recordset) to Ruby primitives.
79
+ def serialize
80
+ if @object
81
+ @many = @object.respond_to?(:each) if @many.nil?
82
+ return @object.as_json(self.get_serializer_config)
78
83
  end
84
+ return nil
85
+ end
79
86
 
80
- # Allow a serializer instance to be used as a hash directly in a nested serializer config.
81
- def [](key)
82
- unless instance_variable_defined?(:@_nested_config)
83
- @_nested_config = self.get_serializer_config
84
- end
85
- return @_nested_config[key]
87
+ # Allow a serializer instance to be used as a hash directly in a nested serializer config.
88
+ def [](key)
89
+ unless instance_variable_defined?(:@_nested_config)
90
+ @_nested_config = self.get_serializer_config
86
91
  end
87
- def []=(key, value)
88
- unless instance_variable_defined?(:@_nested_config)
89
- @_nested_config = self.get_serializer_config
90
- end
91
- return @_nested_config[key] = value
92
+ return @_nested_config[key]
93
+ end
94
+ def []=(key, value)
95
+ unless instance_variable_defined?(:@_nested_config)
96
+ @_nested_config = self.get_serializer_config
92
97
  end
98
+ return @_nested_config[key] = value
99
+ end
93
100
 
94
- # Allow a serializer class to be used as a hash directly in a nested serializer config.
95
- def self.[](key)
96
- unless instance_variable_defined?(:@_nested_config)
97
- @_nested_config = self.new.get_serializer_config
98
- end
99
- return @_nested_config[key]
101
+ # Allow a serializer class to be used as a hash directly in a nested serializer config.
102
+ def self.[](key)
103
+ unless instance_variable_defined?(:@_nested_config)
104
+ @_nested_config = self.new.get_serializer_config
100
105
  end
101
- def self.[]=(key, value)
102
- unless instance_variable_defined?(:@_nested_config)
103
- @_nested_config = self.new.get_serializer_config
104
- end
105
- return @_nested_config[key] = value
106
+ return @_nested_config[key]
107
+ end
108
+ def self.[]=(key, value)
109
+ unless instance_variable_defined?(:@_nested_config)
110
+ @_nested_config = self.new.get_serializer_config
106
111
  end
112
+ return @_nested_config[key] = value
107
113
  end
108
114
  end