rest_framework 0.0.12 → 0.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8c3691667e86ee77e9d15a1732d0013687a2e63a062a4ff01d1c6d8cc66f45fc
4
- data.tar.gz: 6bcab926c283feb9a0643a06eab750f941882e5b56b294c59794f64a966883bb
3
+ metadata.gz: 46db4e7aa05600caaa4c1f9f645f417b4d3cd9e5bcde7da0b54d3c5fa84e872e
4
+ data.tar.gz: 95c9d018b687795f80c38b43289f19ffd44eb1b170d883c164b590ab6b1c20d0
5
5
  SHA512:
6
- metadata.gz: 204e0b5ea7b46710968a6274791f9e0f676db34f4eaefa1cf831fe8f9e4ab1f076a24b0375ef0554e1c4b57db3d1787adeebc31c4201da40b504aded51ca097b
7
- data.tar.gz: 7e46ed5bbe61832045207664d679ecf394aead2928cdd78a1b29873087b78130546fd80595fe443680d5c26eebfa8bf5f23246b56a9a4a83e801c7763c024c76
6
+ metadata.gz: 2c376d191ffa5ae9de932dceb8411362a00be789ff9aa3733f038608da890fc5437d981576e3b17d402b857f7966cb15e437d7cbe2c8447191088a9a2249134f
7
+ data.tar.gz: 72e31acb2e66c6d8d2af2dd7375e7732a93b5e66b1c1fedf4c1d781421422c582958d32c6022305cd9cc2ca9dda51e2c2f6c540b1737e11eb3506501ef91b618
data/README.md CHANGED
@@ -116,18 +116,6 @@ To run the full test suite:
116
116
  $ rake test
117
117
  ```
118
118
 
119
- To run unit tests:
120
-
121
- ```shell
122
- $ rake test:unit
123
- ```
124
-
125
- To run integration tests:
126
-
127
- ```shell
128
- $ rake test:integration
129
- ```
130
-
131
- To interact with the integration app, you can `cd test/integration` and operate it via the normal
132
- Rails interfaces. Ensure you run `rake db:schema:load` before running `rails server` or
133
- `rails console`.
119
+ To interact with the test app, `cd test` and operate it via the normal Rails interfaces. Ensure you
120
+ run `rake db:schema:load` before running `rails server` or `rails console`. You can also load the
121
+ test fixtures with `rake db:fixtures:load`.
@@ -30,14 +30,14 @@
30
30
  <% if @json_payload %>
31
31
  <li class="nav-item">
32
32
  <a class="nav-link active" href="#tab-json" data-toggle="tab" role="tab">
33
- JSON
33
+ .json
34
34
  </a>
35
35
  </li>
36
36
  <% end %>
37
37
  <% if @xml_payload %>
38
38
  <li class="nav-item">
39
39
  <a class="nav-link" href="#tab-xml" data-toggle="tab" role="tab">
40
- XML
40
+ .xml
41
41
  </a>
42
42
  </li>
43
43
  <% end %>
@@ -3,5 +3,8 @@ end
3
3
 
4
4
  require_relative "rest_framework/controller_mixins"
5
5
  require_relative "rest_framework/engine"
6
+ require_relative "rest_framework/filters"
7
+ require_relative "rest_framework/paginators"
6
8
  require_relative "rest_framework/routers"
9
+ require_relative "rest_framework/serializers"
7
10
  require_relative "rest_framework/version"
@@ -1 +1 @@
1
- 0.0.12
1
+ 0.1.0
@@ -1,3 +1,5 @@
1
+ require_relative '../serializers'
2
+
1
3
  module RESTFramework
2
4
 
3
5
  # This module provides the common functionality for any controller mixins, a `root` action, and
@@ -28,12 +30,40 @@ module RESTFramework
28
30
  def self.included(base)
29
31
  if base.is_a? Class
30
32
  base.extend ClassMethods
31
- base.class_attribute(*[
32
- :singleton_controller,
33
- :extra_actions,
34
- :skip_actions,
35
- :paginator_class,
36
- ])
33
+
34
+ # Add class attributes (with defaults) unless they already exist.
35
+ {
36
+ extra_actions: nil,
37
+ extra_member_actions: nil,
38
+ filter_backends: nil,
39
+ paginator_class: nil,
40
+ page_size: nil,
41
+ page_query_param: 'page',
42
+ page_size_query_param: 'page_size',
43
+ max_page_size: nil,
44
+ serializer_class: nil,
45
+ singleton_controller: nil,
46
+ skip_actions: nil,
47
+ }.each do |a, default|
48
+ unless base.respond_to?(a)
49
+ base.class_attribute(a)
50
+
51
+ # Set default manually so we can still support Rails 4. Maybe later we can use the
52
+ # default parameter on `class_attribute`.
53
+ base.send(:"#{a}=", default)
54
+ end
55
+ end
56
+
57
+ # Alias `extra_actions` to `extra_collection_actions`.
58
+ unless base.respond_to?(:extra_collection_actions)
59
+ base.alias_method(:extra_collection_actions, :extra_actions)
60
+ base.alias_method(:extra_collection_actions=, :extra_actions=)
61
+ end
62
+
63
+ # skip csrf since this is an API
64
+ base.skip_before_action(:verify_authenticity_token) rescue nil
65
+
66
+ # handle some common exceptions
37
67
  base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
38
68
  base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
39
69
  base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
@@ -43,6 +73,40 @@ module RESTFramework
43
73
 
44
74
  protected
45
75
 
76
+ # Helper to get filtering backends with a sane default.
77
+ def get_filter_backends
78
+ return self.class.filter_backends || []
79
+ end
80
+
81
+ # Filter the recordset over all configured filter backends.
82
+ def get_filtered_data(data)
83
+ self.get_filter_backends.each do |filter_class|
84
+ filter = filter_class.new(controller: self)
85
+ data = filter.get_filtered_data(data)
86
+ end
87
+
88
+ return data
89
+ end
90
+
91
+ # Helper to get the configured serializer class.
92
+ def get_serializer_class
93
+ return self.class.serializer_class
94
+ end
95
+
96
+ # Get a native serializer config for the current action.
97
+ def get_native_serializer_config
98
+ action_serializer_config = self.class.native_serializer_action_config || {}
99
+ action = self.action_name.to_sym
100
+
101
+ # Handle case where :index action is not defined.
102
+ if action == :index && !action_serializer_config.key?(:index)
103
+ # Default is :show if `singleton_controller`, otherwise :list.
104
+ action = self.class.singleton_controller ? :show : :list
105
+ end
106
+
107
+ return (action_serializer_config[action] if action) || self.class.native_serializer_config
108
+ end
109
+
46
110
  def record_invalid(e)
47
111
  return api_response(
48
112
  {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
@@ -61,6 +125,7 @@ module RESTFramework
61
125
  return api_response({message: "Record not destroyed.", exception: e}, status: 406)
62
126
  end
63
127
 
128
+ # Helper for showing routes under a controller action, used for the browsable API.
64
129
  def _get_routes
65
130
  begin
66
131
  formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
@@ -69,14 +134,14 @@ module RESTFramework
69
134
  end
70
135
  return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
71
136
  formatter.new
72
- ).lines[1..].map { |r| r.split.last(3) }.map { |r|
137
+ ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
73
138
  {verb: r[0], path: r[1], action: r[2]}
74
139
  }.select { |r| r[:path].start_with?(request.path) }
75
140
  end
76
141
 
77
142
  # Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
78
143
  # primitives.
79
- def api_response(payload=nil, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
144
+ def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
80
145
  html_kwargs ||= {}
81
146
  json_kwargs ||= {}
82
147
  xml_kwargs ||= {}
@@ -119,8 +184,8 @@ module RESTFramework
119
184
  rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
120
185
  kwargs[:layout] = "rest_framework"
121
186
  kwargs[:template] = "rest_framework/default"
187
+ render(**kwargs)
122
188
  end
123
- render(**kwargs)
124
189
  }
125
190
  end
126
191
  end
@@ -1,5 +1,5 @@
1
1
  require_relative 'base'
2
- require_relative '../serializers'
2
+ require_relative '../filters'
3
3
 
4
4
  module RESTFramework
5
5
 
@@ -8,95 +8,94 @@ module RESTFramework
8
8
  def self.included(base)
9
9
  if base.is_a? Class
10
10
  BaseControllerMixin.included(base)
11
- base.class_attribute(*[
12
- :model,
13
- :recordset,
14
- :fields,
15
- :action_fields,
16
- :native_serializer_config,
17
- :native_serializer_action_config,
18
- :filterset_fields,
19
- :allowed_parameters,
20
- :allowed_action_parameters,
21
- :serializer_class,
22
- :extra_member_actions,
23
- ])
24
- base.alias_method(:extra_collection_actions=, :extra_actions=)
11
+
12
+ # Add class attributes (with defaults) unless they already exist.
13
+ {
14
+ # Core attributes related to models.
15
+ model: nil,
16
+ recordset: nil,
17
+
18
+ # Attributes for create/update parameters.
19
+ allowed_parameters: nil,
20
+ allowed_action_parameters: nil,
21
+
22
+ # Attributes for configuring record fields.
23
+ fields: nil,
24
+ action_fields: nil,
25
+
26
+ # Attributes for the default native serializer.
27
+ native_serializer_config: nil,
28
+ native_serializer_action_config: nil,
29
+
30
+ # Attributes for default model filtering (and ordering).
31
+ filterset_fields: nil,
32
+ ordering_fields: nil,
33
+ ordering_query_param: 'ordering',
34
+
35
+ # Other misc attributes.
36
+ disable_creation_from_recordset: nil, # Option to disable `recordset.create` behavior.
37
+ }.each do |a, default|
38
+ unless base.respond_to?(a)
39
+ base.class_attribute(a)
40
+
41
+ # Set default manually so we can still support Rails 4. Maybe later we can use the
42
+ # default parameter on `class_attribute`.
43
+ base.send(:"#{a}=", default)
44
+ end
45
+ end
25
46
  end
26
47
  end
27
48
 
28
49
  protected
29
50
 
51
+ # Helper to get the configured serializer class, or `NativeModelSerializer` as a default.
30
52
  def get_serializer_class
31
- return self.class.serializer_class || NativeModelSerializer
53
+ return self.class.serializer_class || RESTFramework::NativeModelSerializer
32
54
  end
33
55
 
34
- # Get a list of fields for the current action.
35
- def get_fields
36
- action_fields = self.class.action_fields || {}
56
+ # Get a list of parameters allowed for the current action.
57
+ def get_allowed_parameters
58
+ allowed_action_parameters = self.class.allowed_action_parameters || {}
37
59
  action = self.action_name.to_sym
38
60
 
39
- # index action should use :list fields if :index is not provided
40
- action = :list if action == :index && !action_fields.key?(:index)
61
+ # index action should use :list allowed parameters if :index is not provided
62
+ action = :list if action == :index && !allowed_action_parameters.key?(:index)
41
63
 
42
- return (action_fields[action] if action) || self.class.fields || []
64
+ return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
43
65
  end
44
66
 
45
- # Get a native serializer config for the current action.
46
- def get_native_serializer_config
47
- action_serializer_config = self.class.native_serializer_action_config || {}
48
- action = self.action_name.to_sym
49
-
50
- # index action should use :list serializer config if :index is not provided
51
- action = :list if action == :index && !action_serializer_config.key?(:index)
52
-
53
- return (action_serializer_config[action] if action) || self.class.native_serializer_config
67
+ # Get the list of filtering backends to use.
68
+ def get_filter_backends
69
+ return self.class.filter_backends || [
70
+ RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
71
+ ]
54
72
  end
55
73
 
56
- # Get a list of parameters allowed for the current action.
57
- def get_allowed_parameters
58
- allowed_action_parameters = self.class.allowed_action_parameters || {}
74
+ # Get a list of fields for the current action.
75
+ def get_fields
76
+ action_fields = self.class.action_fields || {}
59
77
  action = self.action_name.to_sym
60
78
 
61
- # index action should use :list allowed parameters if :index is not provided
62
- action = :list if action == :index && !allowed_action_parameters.key?(:index)
79
+ # index action should use :list fields if :index is not provided
80
+ action = :list if action == :index && !action_fields.key?(:index)
63
81
 
64
- return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
82
+ return (action_fields[action] if action) || self.class.fields || []
65
83
  end
66
84
 
67
85
  # Filter the request body for keys in current action's allowed_parameters/fields config.
68
- def _get_parameter_values_from_request_body
86
+ def get_body_params
69
87
  fields = self.get_allowed_parameters || self.get_fields
70
- return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
88
+ return @get_body_params ||= (request.request_parameters.select { |p|
71
89
  fields.include?(p.to_sym) || fields.include?(p.to_s)
72
90
  })
73
91
  end
74
- alias :get_create_params :_get_parameter_values_from_request_body
75
- alias :get_update_params :_get_parameter_values_from_request_body
76
-
77
- # Filter params for keys allowed by the current action's filterset_fields/fields config.
78
- def _get_filterset_values_from_params
79
- fields = self.filterset_fields || self.get_fields
80
- return @_get_filterset_values_from_params ||= request.query_parameters.select { |p|
81
- fields.include?(p.to_sym) || fields.include?(p.to_s)
82
- }
83
- end
84
- alias :get_lookup_params :_get_filterset_values_from_params
85
- alias :get_filter_params :_get_filterset_values_from_params
86
-
87
- # Get the recordset, filtered by the filter params.
88
- def get_filtered_recordset
89
- filter_params = self.get_filter_params
90
- unless filter_params.blank?
91
- return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
92
- end
93
- return self.get_recordset
94
- end
92
+ alias :get_create_params :get_body_params
93
+ alias :get_update_params :get_body_params
95
94
 
96
95
  # Get a record by `id` or return a single record if recordset is filtered down to a single
97
96
  # record.
98
97
  def get_record
99
- records = self.get_filtered_recordset
98
+ records = self.get_filtered_data(self.get_recordset)
100
99
  if params['id'] # direct lookup
101
100
  return records.find(params['id'])
102
101
  elsif records.length == 1
@@ -143,33 +142,42 @@ module RESTFramework
143
142
  end
144
143
 
145
144
  module ListModelMixin
146
- # TODO: pagination classes like Django
147
145
  def index
148
- @records = self.get_filtered_recordset
149
- @serialized_records = self.get_serializer_class.new(
150
- object: @records, controller: self
151
- ).serialize
152
- return api_response(@serialized_records)
146
+ @records = self.get_filtered_data(self.get_recordset)
147
+
148
+ # Handle pagination, if enabled.
149
+ if self.class.paginator_class
150
+ paginator = self.class.paginator_class.new(data: @records, controller: self)
151
+ page = paginator.get_page
152
+ serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
153
+ data = paginator.get_paginated_response(serialized_page)
154
+ else
155
+ data = self.get_serializer_class.new(object: @records, controller: self).serialize
156
+ end
157
+
158
+ return api_response(data)
153
159
  end
154
160
  end
155
161
 
156
162
  module ShowModelMixin
157
163
  def show
158
164
  @record = self.get_record
159
- @serialized_record = self.get_serializer_class.new(
160
- object: @record, controller: self
161
- ).serialize
162
- return api_response(@serialized_record)
165
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
166
+ return api_response(serialized_record)
163
167
  end
164
168
  end
165
169
 
166
170
  module CreateModelMixin
167
171
  def create
168
- @record = self.get_model.create!(self.get_create_params)
169
- @serialized_record = self.get_serializer_class.new(
170
- object: @record, controller: self
171
- ).serialize
172
- return api_response(@serialized_record)
172
+ if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
173
+ # Create with any properties inherited from the recordset (like associations).
174
+ @record = self.get_recordset.create!(self.get_create_params)
175
+ else
176
+ # Otherwise, perform a "bare" create.
177
+ @record = self.get_model.create!(self.get_create_params)
178
+ end
179
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
180
+ return api_response(serialized_record)
173
181
  end
174
182
  end
175
183
 
@@ -177,10 +185,8 @@ module RESTFramework
177
185
  def update
178
186
  @record = self.get_record
179
187
  @record.update!(self.get_update_params)
180
- @serialized_record = self.get_serializer_class.new(
181
- object: @record, controller: self
182
- ).serialize
183
- return api_response(@serialized_record)
188
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
189
+ return api_response(serialized_record)
184
190
  end
185
191
  end
186
192
 
@@ -0,0 +1,65 @@
1
+ module RESTFramework
2
+ class BaseFilter
3
+ def initialize(controller:)
4
+ @controller = controller
5
+ end
6
+
7
+ def get_filtered_data(data)
8
+ raise NotImplementedError
9
+ end
10
+ end
11
+
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
+
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
28
+
29
+ return data
30
+ end
31
+ end
32
+
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
+
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
49
+
50
+ return nil
51
+ end
52
+
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
60
+ end
61
+
62
+ # TODO: implement searching within fields rather than exact match filtering (ModelFilter)
63
+ # class ModelSearchFilter < BaseFilter
64
+ # end
65
+ end
@@ -0,0 +1,84 @@
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 != 0)
15
+ end
16
+
17
+ def _page_size
18
+ page_size = nil
19
+
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
27
+
28
+ # Otherwise, get from config.
29
+ if !page_size && @controller.class.page_size
30
+ page_size = @controller.class.page_size
31
+ end
32
+
33
+ # Fallback to a page size of 15.
34
+ page_size = 15 unless page_size
35
+
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
39
+ end
40
+
41
+ # Ensure we return at least 1.
42
+ return page_size.zero? ? 1 : page_size
43
+ end
44
+
45
+ def _page_query_param
46
+ return @controller.class.page_query_param&.to_sym
47
+ end
48
+
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?
54
+ page_number = 1
55
+ else
56
+ page_number = page_number.to_i
57
+ if page_number.zero?
58
+ page_number = 1
59
+ end
60
+ end
61
+ 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
+ end
68
+
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
78
+ end
79
+
80
+ # TODO: implement this
81
+ # class CountOffsetPaginator
82
+ # end
83
+
84
+ end
@@ -2,6 +2,41 @@ require 'action_dispatch/routing/mapper'
2
2
 
3
3
  module ActionDispatch::Routing
4
4
  class Mapper
5
+ # 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
+
5
40
  # Private 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
@@ -40,16 +75,16 @@ module ActionDispatch::Routing
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
169
  # @param label [Symbol] the snake_case name of the controller
136
- def rest_root(path=nil, **kwargs, &block)
137
- # by default, use RootController#root
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
@@ -2,25 +2,30 @@ module RESTFramework
2
2
  class BaseSerializer
3
3
  attr_reader :errors
4
4
 
5
- def initialize(object: nil, data: nil, controller: nil, **kwargs)
5
+ def initialize(object: nil, controller: nil, **kwargs)
6
6
  @object = object
7
- @data = data
8
7
  @controller = controller
9
8
  end
10
9
  end
11
10
 
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).
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).
15
14
  class NativeModelSerializer < BaseSerializer
16
15
  class_attribute :config
17
16
  class_attribute :singular_config
18
17
  class_attribute :plural_config
19
18
  class_attribute :action_config
20
19
 
21
- def initialize(model: nil, many: nil, **kwargs)
20
+ def initialize(many: nil, model: nil, **kwargs)
22
21
  super(**kwargs)
23
- @many = many
22
+
23
+ if many.nil?
24
+ @many = @object.respond_to?(:count) ? @object.count : nil
25
+ else
26
+ @many = many
27
+ end
28
+
24
29
  @model = model || (@controller ? @controller.send(:get_model) : nil)
25
30
  end
26
31
 
@@ -34,32 +39,34 @@ module RESTFramework
34
39
  action = self.get_action
35
40
 
36
41
  if action && self.action_config
37
- # index action should use :list serializer config if :index is not provided
42
+ # Index action should use :list serializer config if :index is not provided.
38
43
  action = :list if action == :index && !self.action_config.key?(:index)
39
44
 
40
45
  return self.action_config[action] if self.action_config[action]
41
46
  end
42
47
 
43
- # no action_config, so try singular/plural config
48
+ # No action_config, so try singular/plural config.
44
49
  return self.plural_config if @many && self.plural_config
45
50
  return self.singular_config if !@many && self.singular_config
46
51
 
47
- # lastly, try the default config
52
+ # Lastly, try returning the default config.
48
53
  return self.config
49
54
  end
50
55
 
51
- # Get a configuration passable to `as_json` for the model.
56
+ # Get a configuration passable to `as_json` for the object.
52
57
  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
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
56
62
 
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
63
+ # Return a serializer config if one is defined.
64
+ if serializer_config = @controller.send(:get_native_serializer_config)
65
+ return serializer_config
66
+ end
60
67
 
61
- # otherwise, build a serializer config from fields
62
- fields = @controller.send(:get_fields) if @controller
68
+ # If the config wasn't determined, build a serializer config from model fields.
69
+ fields = @controller.try(:get_fields) if @controller
63
70
  unless fields.blank?
64
71
  columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
65
72
  return {only: columns, methods: methods}
@@ -68,7 +75,7 @@ module RESTFramework
68
75
  return {}
69
76
  end
70
77
 
71
- # Convert the object(s) to Ruby primitives.
78
+ # Convert the object (record or recordset) to Ruby primitives.
72
79
  def serialize
73
80
  if @object
74
81
  @many = @object.respond_to?(:each) if @many.nil?
@@ -105,4 +112,5 @@ module RESTFramework
105
112
  return @_nested_config[key] = value
106
113
  end
107
114
  end
115
+
108
116
  end
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.0.12
4
+ version: 0.1.0
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: 2020-10-09 00:00:00.000000000 Z
11
+ date: 2021-02-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -43,14 +43,16 @@ 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/filters.rb
47
+ - lib/rest_framework/paginators.rb
46
48
  - lib/rest_framework/routers.rb
47
49
  - lib/rest_framework/serializers.rb
48
50
  - lib/rest_framework/version.rb
49
- homepage: https://github.com/gregschmit/rails-rest-framework
51
+ homepage: https://rails-rest-framework.com
50
52
  licenses:
51
53
  - MIT
52
54
  metadata:
53
- homepage_uri: https://github.com/gregschmit/rails-rest-framework
55
+ homepage_uri: https://rails-rest-framework.com
54
56
  source_code_uri: https://github.com/gregschmit/rails-rest-framework
55
57
  post_install_message:
56
58
  rdoc_options: []
@@ -68,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
68
70
  - !ruby/object:Gem::Version
69
71
  version: '0'
70
72
  requirements: []
71
- rubygems_version: 3.0.8
73
+ rubygems_version: 3.0.9
72
74
  signing_key:
73
75
  specification_version: 4
74
76
  summary: A framework for DRY RESTful APIs in Ruby on Rails.