rest_framework 0.0.10 → 0.0.16

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: 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