rest_framework 0.0.12 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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.