rest_framework 0.0.10 → 0.0.16

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: a3ed68df38881608ec5cc6c66c286624a600398a745ab730e6cef89513f0138f
4
- data.tar.gz: 7f2654b29027aa24357b14082e0e0236d9024521fe47e3ae90be7fdeda535b5f
3
+ metadata.gz: 825e7e7ac0e9c8ae57250288a54dc3ef6510665ce3d4fee23245c559aa8e8233
4
+ data.tar.gz: 2211c303d14708ef94a4cb35d6e3b25bf78b3b08645f2990ca5448354d8dd64e
5
5
  SHA512:
6
- metadata.gz: 460fd09cc0b375e5509a83ff64bf5ffb87f864cb53e5cb51aa1027bce3a39bbd12a8f692a3c621ad93ac200f98bb095b369610b2953eedb9a47a181568fea948
7
- data.tar.gz: 03c6ec7c3d910dac783e2ddb927ea53212e066246dcf1b2673d95e703754a9295a5e792d9daeb3c40fd5d334491e489e076b4740f766ab7880eaf3d731344bda
6
+ metadata.gz: 0ee2cf406f2b8d77d9ea63650b080d037a759143e0a3e19846585d493a386a4647db0fe59188aa0fd887257a18087aaca82b80d99dee962af1dfdb7eca66a07c
7
+ data.tar.gz: 6248caea5ad7f5a37a55f7da6aae55c70dc284b9f04d046fc6149a13dff4ae34410106c97f54274f59a8353c79fe97073b7dcba51d65d9c94eb0f9cff1a205ac
data/README.md CHANGED
@@ -116,18 +116,5 @@ 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`.
@@ -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 %>
@@ -46,7 +46,7 @@
46
46
  <div class="tab-content w-100 pt-3">
47
47
  <div class="tab-pane fade show active" id="tab-json" role="tab">
48
48
  <% if @json_payload %>
49
- <div><pre><code class="language-json"><%= JSON.pretty_generate(JSON.parse(@json_payload)) %></code></pre></div>
49
+ <div><pre><code class="language-json"><%= JSON.pretty_generate(JSON.parse(@json_payload)) unless @json_payload == '' %></code></pre></div>
50
50
  <% end %>
51
51
  </div>
52
52
  <div class="tab-pane fade" id="tab-xml" role="tab">
@@ -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.10
1
+ 0.0.16
@@ -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,42 @@ 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
+ native_serializer_config: nil,
40
+ native_serializer_action_config: nil,
41
+ paginator_class: nil,
42
+ page_size: nil,
43
+ page_query_param: 'page',
44
+ page_size_query_param: 'page_size',
45
+ max_page_size: nil,
46
+ serializer_class: nil,
47
+ singleton_controller: nil,
48
+ skip_actions: nil,
49
+ }.each do |a, default|
50
+ unless base.respond_to?(a)
51
+ base.class_attribute(a)
52
+
53
+ # Set default manually so we can still support Rails 4. Maybe later we can use the
54
+ # default parameter on `class_attribute`.
55
+ base.send(:"#{a}=", default)
56
+ end
57
+ end
58
+
59
+ # Alias `extra_actions` to `extra_collection_actions`.
60
+ unless base.respond_to?(:extra_collection_actions)
61
+ base.alias_method(:extra_collection_actions, :extra_actions)
62
+ base.alias_method(:extra_collection_actions=, :extra_actions=)
63
+ end
64
+
65
+ # skip csrf since this is an API
66
+ base.skip_before_action(:verify_authenticity_token) rescue nil
67
+
68
+ # handle some common exceptions
37
69
  base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
38
70
  base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
39
71
  base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
@@ -43,6 +75,45 @@ module RESTFramework
43
75
 
44
76
  protected
45
77
 
78
+ # Helper to get filtering backends with a sane default.
79
+ def get_filter_backends
80
+ if self.class.filter_backends
81
+ return self.class.filter_backends
82
+ end
83
+
84
+ # By default, return nil.
85
+ return nil
86
+ end
87
+
88
+ # Filter the recordset over all configured filter backends.
89
+ def get_filtered_data(data)
90
+ (self.get_filter_backends || []).each do |filter_class|
91
+ filter = filter_class.new(controller: self)
92
+ data = filter.get_filtered_data(data)
93
+ end
94
+
95
+ return data
96
+ end
97
+
98
+ # Helper to get the configured serializer class, or `NativeModelSerializer` as a default.
99
+ def get_serializer_class
100
+ return self.class.serializer_class || NativeModelSerializer
101
+ end
102
+
103
+ # Get a native serializer config for the current action.
104
+ def get_native_serializer_config
105
+ action_serializer_config = self.class.native_serializer_action_config || {}
106
+ action = self.action_name.to_sym
107
+
108
+ # Handle case where :index action is not defined.
109
+ if action == :index && !action_serializer_config.key?(:index)
110
+ # Default is :show if `singleton_controller`, otherwise :list.
111
+ action = self.class.singleton_controller ? :show : :list
112
+ end
113
+
114
+ return (action_serializer_config[action] if action) || self.class.native_serializer_config
115
+ end
116
+
46
117
  def record_invalid(e)
47
118
  return api_response(
48
119
  {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
@@ -61,6 +132,7 @@ module RESTFramework
61
132
  return api_response({message: "Record not destroyed.", exception: e}, status: 406)
62
133
  end
63
134
 
135
+ # Helper for showing routes under a controller action, used for the browsable API.
64
136
  def _get_routes
65
137
  begin
66
138
  formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
@@ -69,40 +141,47 @@ module RESTFramework
69
141
  end
70
142
  return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
71
143
  formatter.new
72
- ).lines[1..].map { |r| r.split.last(3) }.map { |r|
144
+ ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
73
145
  {verb: r[0], path: r[1], action: r[2]}
74
146
  }.select { |r| r[:path].start_with?(request.path) }
75
147
  end
76
148
 
77
- # Helper alias for `respond_to`/`render`, and replace nil responses with blank ones. `payload`
78
- # should be already serialized to Ruby primitives.
149
+ # Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
150
+ # primitives.
79
151
  def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
80
152
  html_kwargs ||= {}
81
153
  json_kwargs ||= {}
82
154
  xml_kwargs ||= {}
83
155
 
84
- # make empty responses status 204 unless a status is already explicitly defined
85
- if (payload.nil? || payload == '') && !kwargs.key?(:status)
86
- kwargs[:status] = 204
87
- end
156
+ # allow blank (no-content) responses
157
+ @blank = kwargs[:blank]
88
158
 
89
159
  respond_to do |format|
90
- if payload.respond_to?(:to_json)
91
- format.json {
92
- kwargs = kwargs.merge(json_kwargs)
93
- render(json: payload || '', **kwargs)
94
- }
95
- end
96
- if payload.respond_to?(:to_xml)
97
- format.xml {
98
- kwargs = kwargs.merge(xml_kwargs)
99
- render(xml: payload || '', **kwargs)
100
- }
160
+ if @blank
161
+ format.json {head :no_content}
162
+ format.xml {head :no_content}
163
+ else
164
+ if payload.respond_to?(:to_json)
165
+ format.json {
166
+ kwargs = kwargs.merge(json_kwargs)
167
+ render(json: payload, layout: false, **kwargs)
168
+ }
169
+ end
170
+ if payload.respond_to?(:to_xml)
171
+ format.xml {
172
+ kwargs = kwargs.merge(xml_kwargs)
173
+ render(xml: payload, layout: false, **kwargs)
174
+ }
175
+ end
101
176
  end
102
177
  format.html {
103
178
  @payload = payload
104
- @json_payload = payload.to_json
105
- @xml_payload = payload.to_xml
179
+ @json_payload = ''
180
+ @xml_payload = ''
181
+ unless @blank
182
+ @json_payload = payload.to_json if payload.respond_to?(:to_json)
183
+ @xml_payload = payload.to_xml if payload.respond_to?(:to_xml)
184
+ end
106
185
  @template_logo_text ||= "Rails REST Framework"
107
186
  @title ||= self.controller_name.camelize
108
187
  @routes ||= self._get_routes
@@ -112,8 +191,8 @@ module RESTFramework
112
191
  rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
113
192
  kwargs[:layout] = "rest_framework"
114
193
  kwargs[:template] = "rest_framework/default"
194
+ render(**kwargs)
115
195
  end
116
- render(**kwargs)
117
196
  }
118
197
  end
119
198
  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,87 @@ 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 default model filtering (and ordering).
27
+ filterset_fields: nil,
28
+ ordering_fields: nil,
29
+ ordering_query_param: 'ordering',
30
+
31
+ # Other misc attributes.
32
+ disable_creation_from_recordset: nil, # Option to disable `recordset.create` behavior.
33
+ }.each do |a, default|
34
+ unless base.respond_to?(a)
35
+ base.class_attribute(a)
36
+
37
+ # Set default manually so we can still support Rails 4. Maybe later we can use the
38
+ # default parameter on `class_attribute`.
39
+ base.send(:"#{a}=", default)
40
+ end
41
+ end
25
42
  end
26
43
  end
27
44
 
28
45
  protected
29
46
 
30
- def get_serializer_class
31
- return self.class.serializer_class || NativeModelSerializer
32
- end
33
-
34
- # Get a list of fields for the current action.
35
- def get_fields
36
- action_fields = self.class.action_fields || {}
47
+ # Get a list of parameters allowed for the current action.
48
+ def get_allowed_parameters
49
+ allowed_action_parameters = self.class.allowed_action_parameters || {}
37
50
  action = self.action_name.to_sym
38
51
 
39
- # index action should use :list fields if :index is not provided
40
- action = :list if action == :index && !action_fields.key?(:index)
52
+ # index action should use :list allowed parameters if :index is not provided
53
+ action = :list if action == :index && !allowed_action_parameters.key?(:index)
41
54
 
42
- return (action_fields[action] if action) || self.class.fields || []
55
+ return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
43
56
  end
44
57
 
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)
58
+ # Get the list of filtering backends to use.
59
+ def get_filter_backends
60
+ backends = super
61
+ return backends if backends
52
62
 
53
- return (action_serializer_config[action] if action) || self.class.native_serializer_config
63
+ # By default, return the standard model filter backend.
64
+ return [RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter]
54
65
  end
55
66
 
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 || {}
67
+ # Get a list of fields for the current action.
68
+ def get_fields
69
+ action_fields = self.class.action_fields || {}
59
70
  action = self.action_name.to_sym
60
71
 
61
- # index action should use :list allowed parameters if :index is not provided
62
- action = :list if action == :index && !allowed_action_parameters.key?(:index)
72
+ # index action should use :list fields if :index is not provided
73
+ action = :list if action == :index && !action_fields.key?(:index)
63
74
 
64
- return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
75
+ return (action_fields[action] if action) || self.class.fields || []
65
76
  end
66
77
 
67
78
  # Filter the request body for keys in current action's allowed_parameters/fields config.
68
79
  def _get_parameter_values_from_request_body
69
80
  fields = self.get_allowed_parameters || self.get_fields
70
- return @_get_field_values_from_request_body ||= (request.request_parameters.select { |p|
81
+ return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
71
82
  fields.include?(p.to_sym) || fields.include?(p.to_s)
72
83
  })
73
84
  end
74
85
  alias :get_create_params :_get_parameter_values_from_request_body
75
86
  alias :get_update_params :_get_parameter_values_from_request_body
76
87
 
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_field_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
95
-
96
88
  # Get a record by `id` or return a single record if recordset is filtered down to a single
97
89
  # record.
98
90
  def get_record
99
- records = self.get_filtered_recordset
91
+ records = self.get_filtered_data(self.get_recordset)
100
92
  if params['id'] # direct lookup
101
93
  return records.find(params['id'])
102
94
  elsif records.length == 1
@@ -143,33 +135,42 @@ module RESTFramework
143
135
  end
144
136
 
145
137
  module ListModelMixin
146
- # TODO: pagination classes like Django
147
138
  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)
139
+ @records = self.get_filtered_data(self.get_recordset)
140
+
141
+ # Handle pagination, if enabled.
142
+ if self.class.paginator_class
143
+ paginator = self.class.paginator_class.new(data: @records, controller: self)
144
+ page = paginator.get_page
145
+ serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
146
+ data = paginator.get_paginated_response(serialized_page)
147
+ else
148
+ data = self.get_serializer_class.new(object: @records, controller: self).serialize
149
+ end
150
+
151
+ return api_response(data)
153
152
  end
154
153
  end
155
154
 
156
155
  module ShowModelMixin
157
156
  def show
158
157
  @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)
158
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
159
+ return api_response(serialized_record)
163
160
  end
164
161
  end
165
162
 
166
163
  module CreateModelMixin
167
164
  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)
165
+ if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
166
+ # Create with any properties inherited from the recordset (like associations).
167
+ @record = self.get_recordset.create!(self.get_create_params)
168
+ else
169
+ # Otherwise, perform a "bare" create.
170
+ @record = self.get_model.create!(self.get_create_params)
171
+ end
172
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
173
+ return api_response(serialized_record)
173
174
  end
174
175
  end
175
176
 
@@ -177,10 +178,8 @@ module RESTFramework
177
178
  def update
178
179
  @record = self.get_record
179
180
  @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)
181
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
182
+ return api_response(serialized_record)
184
183
  end
185
184
  end
186
185
 
@@ -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
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,9 +2,8 @@ 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
@@ -12,16 +11,15 @@ module RESTFramework
12
11
  # This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is a Rails
13
12
  # method which converts objects to Ruby primitives (with the top-level being either an array or a
14
13
  # hash).
15
- class NativeModelSerializer < BaseSerializer
14
+ class NativeSerializer < 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, **kwargs)
22
21
  super(**kwargs)
23
22
  @many = many
24
- @model = model || (@controller ? @controller.send(:get_model) : nil)
25
23
  end
26
24
 
27
25
  # Get controller action, if possible.
@@ -34,88 +32,94 @@ module RESTFramework
34
32
  action = self.get_action
35
33
 
36
34
  if action && self.action_config
37
- # index action should use :list serializer config if :index is not provided
35
+ # Index action should use :list serializer config if :index is not provided.
38
36
  action = :list if action == :index && !self.action_config.key?(:index)
39
37
 
40
38
  return self.action_config[action] if self.action_config[action]
41
39
  end
42
40
 
43
- # no action_config, so try singular/plural config
41
+ # No action_config, so try singular/plural config.
44
42
  return self.plural_config if @many && self.plural_config
45
43
  return self.singular_config if !@many && self.singular_config
46
44
 
47
- # lastly, try the default config
45
+ # Lastly, try returning the default config.
48
46
  return self.config
49
47
  end
50
48
 
51
- # Get a configuration passable to `as_json` for the model.
49
+ # Get a configuration passable to `as_json` for the object.
52
50
  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
56
-
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
51
+ # Return a locally defined serializer config if one is defined.
52
+ if local_config = self.get_local_serializer_config
53
+ return local_config
54
+ end
60
55
 
61
- # otherwise, build a serializer config from fields
62
- fields = @controller.send(:get_fields)
63
- unless fields.blank?
64
- columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
65
- return {only: columns, methods: methods}
56
+ # Return a serializer config if one is defined.
57
+ if serializer_config = @controller.send(:get_native_serializer_config)
58
+ return serializer_config
66
59
  end
67
60
 
68
61
  return {}
69
62
  end
70
63
 
71
- # Recursive method for traversing a config and evaluating nested serializers.
72
- def _resolve_serializer_config(node: nil)
73
- # First base case: found a serializer, so evaluate it and return it.
74
- if node.is_a?(Class) && (node < BaseSerializer)
75
- return node.new(controller: @controller).get_resolved_serializer_config
64
+ # Convert the object (record or recordset) to Ruby primitives.
65
+ def serialize
66
+ if @object
67
+ @many = @object.respond_to?(:each) if @many.nil?
68
+ return @object.as_json(self.get_serializer_config)
76
69
  end
70
+ return nil
71
+ end
77
72
 
78
- # Second base case: found a serializer instance, so evaluate it and return it.
79
- if node.is_a?(BaseSerializer)
80
- return node.get_resolved_serializer_config
73
+ # Allow a serializer instance to be used as a hash directly in a nested serializer config.
74
+ def [](key)
75
+ unless instance_variable_defined?(:@_nested_config)
76
+ @_nested_config = self.get_serializer_config
81
77
  end
82
-
83
- # Third base case: node is not iterable, so return it.
84
- unless node.respond_to?(:each)
85
- return node
78
+ return @_nested_config[key]
79
+ end
80
+ def []=(key, value)
81
+ unless instance_variable_defined?(:@_nested_config)
82
+ @_nested_config = self.get_serializer_config
86
83
  end
84
+ return @_nested_config[key] = value
85
+ end
87
86
 
88
- # Recursive case: node is iterable, so iterate and recursively resolve serializers.
89
- if node.is_a? Hash
90
- node.each do |k,v|
91
- node[k] = self._resolve_serializer_config(node: v)
92
- end
93
- else
94
- node.map! do |v|
95
- self._resolve_serializer_config(node: v)
96
- end
87
+ # Allow a serializer class to be used as a hash directly in a nested serializer config.
88
+ def self.[](key)
89
+ unless instance_variable_defined?(:@_nested_config)
90
+ @_nested_config = self.new.get_serializer_config
97
91
  end
98
-
99
- return node
92
+ return @_nested_config[key]
100
93
  end
94
+ def self.[]=(key, value)
95
+ unless instance_variable_defined?(:@_nested_config)
96
+ @_nested_config = self.new.get_serializer_config
97
+ end
98
+ return @_nested_config[key] = value
99
+ end
100
+ end
101
101
 
102
- # Get a serializer config and resolve any nested serializers into configs.
103
- def get_resolved_serializer_config
104
- config = self.get_serializer_config
105
-
106
- # traverse the config, resolving nested serializers
107
- return _resolve_serializer_config(node: config)
102
+ # `NativeModelSerializer` is similar to `NativeSerializer` but with some customizations to work
103
+ # with `ActiveModel`.
104
+ class NativeModelSerializer < NativeSerializer
105
+ def initialize(model: nil, **kwargs)
106
+ super(**kwargs)
107
+ @model = model || (@controller ? @controller.send(:get_model) : nil)
108
108
  end
109
109
 
110
- # Convert the object(s) to Ruby primitives.
111
- def serialize
112
- if @object
113
- @many = @object.respond_to?(:each) if @many.nil?
114
- return @object.as_json(
115
- self.get_resolved_serializer_config
116
- )
110
+ # Get a configuration passable to `as_json` for the object.
111
+ def get_serializer_config
112
+ config = super
113
+ return config unless config.blank?
114
+
115
+ # If the config wasn't determined, build a serializer config from model fields.
116
+ fields = @controller.try(:get_fields) if @controller
117
+ unless fields.blank?
118
+ columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
119
+ return {only: columns, methods: methods}
117
120
  end
118
- return nil
121
+
122
+ return {}
119
123
  end
120
124
  end
121
125
  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.10
4
+ version: 0.0.16
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-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -43,6 +43,8 @@ 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