rest_framework 0.0.16 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 825e7e7ac0e9c8ae57250288a54dc3ef6510665ce3d4fee23245c559aa8e8233
4
- data.tar.gz: 2211c303d14708ef94a4cb35d6e3b25bf78b3b08645f2990ca5448354d8dd64e
3
+ metadata.gz: 3af970dedaa74cde70348a09e74b190878e4a429a4db0318b093c581f810a349
4
+ data.tar.gz: 5ccd0cf76d51d9d5d3035eee80e73e1234b13c974d04bbd4e80a48bc5b82d714
5
5
  SHA512:
6
- metadata.gz: 0ee2cf406f2b8d77d9ea63650b080d037a759143e0a3e19846585d493a386a4647db0fe59188aa0fd887257a18087aaca82b80d99dee962af1dfdb7eca66a07c
7
- data.tar.gz: 6248caea5ad7f5a37a55f7da6aae55c70dc284b9f04d046fc6149a13dff4ae34410106c97f54274f59a8353c79fe97073b7dcba51d65d9c94eb0f9cff1a205ac
6
+ metadata.gz: a5841e302bdf9f2ae56748c2c81e94338a9c77c0dde248218d48733ff9bda28dc4a42e9cb5b21c339abf596490bb6de21e18a1f43bb8ef80802ce8d8db4d925e
7
+ data.tar.gz: 0b395e37ac08c45e3bf8d3940dde55fba063cb43b3e8125e4c9a55dbf6dc15e985c1fa72e51a042e4563194b0f4fa5f33dd3fa5e96de456eaf9448a27b7f76ad
data/README.md CHANGED
@@ -2,16 +2,22 @@
2
2
 
3
3
  [![Gem Version](https://badge.fury.io/rb/rest_framework.svg)](https://badge.fury.io/rb/rest_framework)
4
4
  [![Build Status](https://travis-ci.org/gregschmit/rails-rest-framework.svg?branch=master)](https://travis-ci.org/gregschmit/rails-rest-framework)
5
+ [![Coverage Status](https://coveralls.io/repos/github/gregschmit/rails-rest-framework/badge.svg?branch=master)](https://coveralls.io/github/gregschmit/rails-rest-framework?branch=master)
5
6
 
6
- Rails REST Framework helps you build awesome Web APIs in Ruby on Rails.
7
+ A framework for DRY RESTful APIs in Ruby on Rails.
7
8
 
8
9
  **The Problem**: Building controllers for APIs usually involves writing a lot of redundant CRUD
9
- logic, and routing them can be obnoxious.
10
+ logic, and routing them can be obnoxious. Building and maintaining features like ordering,
11
+ filtering, and pagination can be tedious.
10
12
 
11
- **The Solution**: This gem handles the common logic so you can focus on the parts of your API which
12
- make it unique.
13
+ **The Solution**: This framework implements browsable API responses, CRUD actions for your models,
14
+ and features like ordering/filtering/pagination, so you can focus on building awesome APIs.
13
15
 
14
- To see detailed documentation, visit https://rails-rest-framework.com.
16
+ Website/Guide: [https://rails-rest-framework.com](https://rails-rest-framework.com)
17
+
18
+ Source: [https://github.com/gregschmit/rails-rest-framework](https://github.com/gregschmit/rails-rest-framework)
19
+
20
+ YARD Docs: [https://rubydoc.info/gems/rest_framework](https://rubydoc.info/gems/rest_framework)
15
21
 
16
22
  ## Installation
17
23
 
@@ -73,8 +79,8 @@ class Api::ReadOnlyMoviesController < ApiController
73
79
  end
74
80
  ```
75
81
 
76
- Note that you can also override `get_model` and `get_recordset` instance methods to override the API
77
- behavior dynamically per-request.
82
+ Note that you can also override the `get_recordset` instance method to override the API behavior
83
+ dynamically per-request.
78
84
 
79
85
  ### Routing
80
86
 
@@ -110,11 +116,12 @@ end
110
116
  After you clone the repository, cd'ing into the directory should create a new gemset if you are
111
117
  using RVM. Then run `bundle install` to install the appropriate gems.
112
118
 
113
- To run the full test suite:
119
+ To run the test suite:
114
120
 
115
121
  ```shell
116
122
  $ rake test
117
123
  ```
118
124
 
119
125
  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`.
126
+ run `rake db:schema:load` before running `rails server` or `rails console`. You can also load the
127
+ test fixtures with `rake db:fixtures:load`.
@@ -1,10 +1,13 @@
1
1
  module RESTFramework
2
2
  end
3
3
 
4
+
4
5
  require_relative "rest_framework/controller_mixins"
5
6
  require_relative "rest_framework/engine"
7
+ require_relative "rest_framework/errors"
6
8
  require_relative "rest_framework/filters"
7
9
  require_relative "rest_framework/paginators"
8
10
  require_relative "rest_framework/routers"
9
11
  require_relative "rest_framework/serializers"
10
12
  require_relative "rest_framework/version"
13
+ require_relative "rest_framework/generators"
@@ -1 +1 @@
1
- 0.0.16
1
+ 0.2.1
@@ -1,2 +1,6 @@
1
+ module RESTFramework::ControllerMixins
2
+ end
3
+
4
+
1
5
  require_relative 'controller_mixins/base'
2
6
  require_relative 'controller_mixins/models'
@@ -1,201 +1,189 @@
1
+ require_relative '../errors'
1
2
  require_relative '../serializers'
2
3
 
3
- module RESTFramework
4
4
 
5
- # This module provides the common functionality for any controller mixins, a `root` action, and
6
- # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
7
- # is defined.
8
- module BaseControllerMixin
9
- # Default action for API root.
10
- def root
11
- api_response({message: "This is the root of your awesome API!"})
12
- end
5
+ # This module provides the common functionality for any controller mixins, a `root` action, and
6
+ # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
7
+ # is defined.
8
+ module RESTFramework::BaseControllerMixin
9
+ # Default action for API root.
10
+ def root
11
+ api_response({message: "This is the root of your awesome API!"})
12
+ end
13
13
 
14
- module ClassMethods
15
- def get_skip_actions(skip_undefined: true)
16
- # first, skip explicitly skipped actions
17
- skip = self.skip_actions || []
14
+ module ClassMethods
15
+ # Helper to get the actions that should be skipped.
16
+ def get_skip_actions(skip_undefined: true)
17
+ # First, skip explicitly skipped actions.
18
+ skip = self.skip_actions || []
18
19
 
19
- # now add methods which don't exist, since we don't want to route those
20
- if skip_undefined
21
- [:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
22
- skip << a unless self.method_defined?(a)
23
- end
20
+ # Now add methods which don't exist, since we don't want to route those.
21
+ if skip_undefined
22
+ [:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
23
+ skip << a unless self.method_defined?(a)
24
24
  end
25
-
26
- return skip
27
25
  end
28
- end
29
26
 
30
- def self.included(base)
31
- if base.is_a? Class
32
- base.extend ClassMethods
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
27
+ return skip
28
+ end
29
+ end
58
30
 
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=)
31
+ def self.included(base)
32
+ if base.is_a? Class
33
+ base.extend ClassMethods
34
+
35
+ # Add class attributes (with defaults) unless they already exist.
36
+ {
37
+ filter_pk_from_request_body: true,
38
+ exclude_body_fields: [:created_at, :created_by, :updated_at, :updated_by],
39
+ accept_generic_params_as_body_params: true,
40
+ extra_actions: nil,
41
+ extra_member_actions: nil,
42
+ filter_backends: nil,
43
+ paginator_class: nil,
44
+ page_size: 20,
45
+ page_query_param: 'page',
46
+ page_size_query_param: 'page_size',
47
+ max_page_size: nil,
48
+ serializer_class: nil,
49
+ serialize_to_json: true,
50
+ serialize_to_xml: true,
51
+ singleton_controller: nil,
52
+ skip_actions: nil,
53
+ }.each do |a, default|
54
+ unless base.respond_to?(a)
55
+ base.class_attribute(a)
56
+
57
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
58
+ # parameter on `class_attribute`.
59
+ base.send(:"#{a}=", default)
63
60
  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
69
- base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
70
- base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
71
- base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
72
- base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
73
61
  end
74
- end
75
-
76
- protected
77
62
 
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
63
+ # Alias `extra_actions` to `extra_collection_actions`.
64
+ unless base.respond_to?(:extra_collection_actions)
65
+ base.alias_method(:extra_collection_actions, :extra_actions)
66
+ base.alias_method(:extra_collection_actions=, :extra_actions=)
82
67
  end
83
68
 
84
- # By default, return nil.
85
- return nil
69
+ # Skip csrf since this is an API.
70
+ base.skip_before_action(:verify_authenticity_token) rescue nil
71
+
72
+ # Handle some common exceptions.
73
+ base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
74
+ base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
75
+ base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
76
+ base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
86
77
  end
78
+ end
87
79
 
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
80
+ protected
94
81
 
95
- return data
96
- end
82
+ # Helper to get filtering backends with a sane default.
83
+ # @return [RESTFramework::BaseFilter]
84
+ def get_filter_backends
85
+ return self.class.filter_backends || []
86
+ end
97
87
 
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
88
+ # Helper to filter an arbitrary data set 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)
101
93
  end
102
94
 
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
95
+ return data
96
+ end
107
97
 
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
98
+ # Helper to get the configured serializer class.
99
+ # @return [RESTFramework::BaseSerializer]
100
+ def get_serializer_class
101
+ return self.class.serializer_class
102
+ end
113
103
 
114
- return (action_serializer_config[action] if action) || self.class.native_serializer_config
115
- end
104
+ def record_invalid(e)
105
+ return api_response(
106
+ {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
107
+ )
108
+ end
116
109
 
117
- def record_invalid(e)
118
- return api_response(
119
- {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
120
- )
121
- end
110
+ def record_not_found(e)
111
+ return api_response({message: "Record not found.", exception: e}, status: 404)
112
+ end
122
113
 
123
- def record_not_found(e)
124
- return api_response({message: "Record not found.", exception: e}, status: 404)
125
- end
114
+ def record_not_saved(e)
115
+ return api_response({message: "Record not saved.", exception: e}, status: 406)
116
+ end
126
117
 
127
- def record_not_saved(e)
128
- return api_response({message: "Record not saved.", exception: e}, status: 406)
129
- end
118
+ def record_not_destroyed(e)
119
+ return api_response({message: "Record not destroyed.", exception: e}, status: 406)
120
+ end
130
121
 
131
- def record_not_destroyed(e)
132
- return api_response({message: "Record not destroyed.", exception: e}, status: 406)
122
+ # Helper for showing routes under a controller action, used for the browsable API.
123
+ def _get_routes
124
+ begin
125
+ formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
126
+ rescue NameError
127
+ formatter = ActionDispatch::Routing::ConsoleFormatter
133
128
  end
129
+ return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
130
+ formatter.new
131
+ ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
132
+ {verb: r[0], path: r[1], action: r[2]}
133
+ }.select { |r| r[:path].start_with?(request.path) }
134
+ end
134
135
 
135
- # Helper for showing routes under a controller action, used for the browsable API.
136
- def _get_routes
137
- begin
138
- formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
139
- rescue NameError
140
- formatter = ActionDispatch::Routing::ConsoleFormatter
141
- end
142
- return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
143
- formatter.new
144
- ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
145
- {verb: r[0], path: r[1], action: r[2]}
146
- }.select { |r| r[:path].start_with?(request.path) }
136
+ # Helper to render a browsable API for `html` format, along with basic `json`/`xml` formats, and
137
+ # with support or passing custom `kwargs` to the underlying `render` calls.
138
+ def api_response(payload, html_kwargs: nil, **kwargs)
139
+ html_kwargs ||= {}
140
+ json_kwargs = kwargs.delete(:json_kwargs) || {}
141
+ xml_kwargs = kwargs.delete(:xml_kwargs) || {}
142
+
143
+ # Raise helpful error if payload is nil. Usually this happens when a record is not found (e.g.,
144
+ # when passing something like `User.find_by(id: some_id)` to `api_response`). The caller should
145
+ # actually be calling `find_by!` to raise ActiveRecord::RecordNotFound and allowing the REST
146
+ # framework to catch this error and return an appropriate error response.
147
+ if payload.nil?
148
+ raise RESTFramework::NilPassedToAPIResponseError
147
149
  end
148
150
 
149
- # Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
150
- # primitives.
151
- def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
152
- html_kwargs ||= {}
153
- json_kwargs ||= {}
154
- xml_kwargs ||= {}
155
-
156
- # allow blank (no-content) responses
157
- @blank = kwargs[:blank]
158
-
159
- respond_to do |format|
160
- if @blank
161
- format.json {head :no_content}
162
- format.xml {head :no_content}
151
+ respond_to do |format|
152
+ if payload == ''
153
+ format.json {head :no_content} if self.serialize_to_json
154
+ format.xml {head :no_content} if self.serialize_to_xml
155
+ else
156
+ format.json {
157
+ jkwargs = kwargs.merge(json_kwargs)
158
+ render(json: payload, layout: false, **jkwargs)
159
+ } if self.serialize_to_json
160
+ format.xml {
161
+ xkwargs = kwargs.merge(xml_kwargs)
162
+ render(xml: payload, layout: false, **xkwargs)
163
+ } if self.serialize_to_xml
164
+ # TODO: possibly support more formats here if supported?
165
+ end
166
+ format.html {
167
+ @payload = payload
168
+ if payload == ''
169
+ @json_payload = '' if self.serialize_to_json
170
+ @xml_payload = '' if self.serialize_to_xml
163
171
  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
172
+ @json_payload = payload.to_json if self.serialize_to_json
173
+ @xml_payload = payload.to_xml if self.serialize_to_xml
176
174
  end
177
- format.html {
178
- @payload = payload
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
185
- @template_logo_text ||= "Rails REST Framework"
186
- @title ||= self.controller_name.camelize
187
- @routes ||= self._get_routes
188
- kwargs = kwargs.merge(html_kwargs)
189
- begin
190
- render(**kwargs)
191
- rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
192
- kwargs[:layout] = "rest_framework"
193
- kwargs[:template] = "rest_framework/default"
194
- render(**kwargs)
195
- end
196
- }
197
- end
175
+ @template_logo_text ||= "Rails REST Framework"
176
+ @title ||= self.controller_name.camelize
177
+ @routes ||= self._get_routes
178
+ hkwargs = kwargs.merge(html_kwargs)
179
+ begin
180
+ render(**hkwargs)
181
+ rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
182
+ hkwargs[:layout] = "rest_framework"
183
+ hkwargs[:template] = "rest_framework/default"
184
+ render(**hkwargs)
185
+ end
186
+ }
198
187
  end
199
188
  end
200
-
201
189
  end
@@ -1,221 +1,267 @@
1
1
  require_relative 'base'
2
2
  require_relative '../filters'
3
3
 
4
- module RESTFramework
5
-
6
- module BaseModelControllerMixin
7
- include BaseControllerMixin
8
- def self.included(base)
9
- if base.is_a? Class
10
- BaseControllerMixin.included(base)
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
4
+
5
+ # This module provides the core functionality for controllers based on models.
6
+ module RESTFramework::BaseModelControllerMixin
7
+ include RESTFramework::BaseControllerMixin
8
+
9
+ def self.included(base)
10
+ if base.is_a? Class
11
+ RESTFramework::BaseControllerMixin.included(base)
12
+
13
+ # Add class attributes (with defaults) unless they already exist.
14
+ {
15
+ # Core attributes related to models.
16
+ model: nil,
17
+ recordset: nil,
18
+
19
+ # Attributes for configuring record fields.
20
+ fields: nil,
21
+ action_fields: nil,
22
+
23
+ # Attributes for create/update parameters.
24
+ allowed_parameters: nil,
25
+ allowed_action_parameters: nil,
26
+
27
+ # Attributes for the default native serializer.
28
+ native_serializer_config: nil,
29
+ native_serializer_singular_config: nil,
30
+ native_serializer_plural_config: nil,
31
+
32
+ # Attributes for default model filtering (and ordering).
33
+ filterset_fields: nil,
34
+ ordering_fields: nil,
35
+ ordering_query_param: 'ordering',
36
+
37
+ # Other misc attributes.
38
+ disable_creation_from_recordset: false, # Option to disable `recordset.create` behavior.
39
+ }.each do |a, default|
40
+ unless base.respond_to?(a)
41
+ base.class_attribute(a)
42
+
43
+ # Set default manually so we can still support Rails 4. Maybe later we can use the default
44
+ # parameter on `class_attribute`.
45
+ base.send(:"#{a}=", default)
41
46
  end
42
47
  end
43
48
  end
49
+ end
44
50
 
45
- protected
51
+ protected
46
52
 
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 || {}
50
- action = self.action_name.to_sym
53
+ def _get_specific_action_config(action_config_key, generic_config_key)
54
+ action_config = self.class.send(action_config_key) || {}
55
+ action = self.action_name&.to_sym
51
56
 
52
- # index action should use :list allowed parameters if :index is not provided
53
- action = :list if action == :index && !allowed_action_parameters.key?(:index)
57
+ # Index action should use :list serializer if :index is not provided.
58
+ action = :list if action == :index && !action_config.key?(:index)
54
59
 
55
- return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
56
- end
57
-
58
- # Get the list of filtering backends to use.
59
- def get_filter_backends
60
- backends = super
61
- return backends if backends
60
+ return (action_config[action] if action) || self.class.send(generic_config_key)
61
+ end
62
62
 
63
- # By default, return the standard model filter backend.
64
- return [RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter]
65
- end
63
+ # Get a list of parameters allowed for the current action.
64
+ def get_allowed_parameters
65
+ return _get_specific_action_config(:allowed_action_parameters, :allowed_parameters)&.map(&:to_s)
66
+ end
66
67
 
67
- # Get a list of fields for the current action.
68
- def get_fields
69
- action_fields = self.class.action_fields || {}
70
- action = self.action_name.to_sym
68
+ # Get a list of fields for the current action.
69
+ def get_fields
70
+ return (
71
+ _get_specific_action_config(:action_fields, :fields)&.map(&:to_s) ||
72
+ self.get_model&.column_names ||
73
+ []
74
+ )
75
+ end
71
76
 
72
- # index action should use :list fields if :index is not provided
73
- action = :list if action == :index && !action_fields.key?(:index)
77
+ # Helper to get the configured serializer class, or `NativeSerializer` as a default.
78
+ # @return [RESTFramework::BaseSerializer]
79
+ def get_serializer_class
80
+ return self.class.serializer_class || RESTFramework::NativeSerializer
81
+ end
74
82
 
75
- return (action_fields[action] if action) || self.class.fields || []
76
- end
83
+ # Get the list of filtering backends to use.
84
+ # @return [RESTFramework::BaseFilter]
85
+ def get_filter_backends
86
+ return self.class.filter_backends || [
87
+ RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
88
+ ]
89
+ end
77
90
 
78
- # Filter the request body for keys in current action's allowed_parameters/fields config.
79
- def _get_parameter_values_from_request_body
91
+ # Filter the request body for keys in current action's allowed_parameters/fields config.
92
+ def get_body_params
93
+ return @_get_body_params ||= begin
80
94
  fields = self.get_allowed_parameters || self.get_fields
81
- return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
82
- fields.include?(p.to_sym) || fields.include?(p.to_s)
83
- })
84
- end
85
- alias :get_create_params :_get_parameter_values_from_request_body
86
- alias :get_update_params :_get_parameter_values_from_request_body
87
-
88
- # Get a record by `id` or return a single record if recordset is filtered down to a single
89
- # record.
90
- def get_record
91
- records = self.get_filtered_data(self.get_recordset)
92
- if params['id'] # direct lookup
93
- return records.find(params['id'])
94
- elsif records.length == 1
95
- return records[0]
96
- end
97
- return nil
98
- end
99
95
 
100
- # Internal interface for get_model, protecting against infinite recursion with get_recordset.
101
- def _get_model(from_internal_get_recordset: false)
102
- return @model if instance_variable_defined?(:@model) && @model
103
- return self.class.model if self.class.model
104
- unless from_internal_get_recordset # prevent infinite recursion
105
- recordset = self._get_recordset(from_internal_get_model: true)
106
- return (@model = recordset.klass) if recordset
96
+ # Filter the request body.
97
+ body_params = request.request_parameters.select { |p|
98
+ fields.include?(p)
99
+ }
100
+
101
+ # Add query params in place of missing body params, if configured.
102
+ if self.class.accept_generic_params_as_body_params
103
+ (fields - body_params.keys).each do |k|
104
+ if (value = params[k])
105
+ body_params[k] = value
106
+ end
107
+ end
107
108
  end
108
- begin
109
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
110
- rescue NameError
109
+
110
+ # Filter primary key if configured.
111
+ if self.class.filter_pk_from_request_body
112
+ body_params.delete(self.get_model&.primary_key)
111
113
  end
112
- return nil
113
- end
114
114
 
115
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
116
- def _get_recordset(from_internal_get_model: false)
117
- return @recordset if instance_variable_defined?(:@recordset) && @recordset
118
- return self.class.recordset if self.class.recordset
119
- unless from_internal_get_model # prevent infinite recursion
120
- model = self._get_model(from_internal_get_recordset: true)
121
- return (@recordset = model.all) if model
115
+ # Filter fields in exclude_body_fields.
116
+ (self.class.exclude_body_fields || []).each do |f|
117
+ body_params.delete(f.to_s)
122
118
  end
123
- return nil
124
- end
125
119
 
126
- # Get the model for this controller.
127
- def get_model
128
- return _get_model
120
+ body_params
129
121
  end
122
+ end
123
+ alias :get_create_params :get_body_params
124
+ alias :get_update_params :get_body_params
130
125
 
131
- # Get the set of records this controller has access to.
132
- def get_recordset
133
- return _get_recordset
126
+ # Get a record by the primary key from the (non-filtered) recordset.
127
+ def get_record
128
+ if pk = params[self.get_model.primary_key]
129
+ return self.get_recordset.find(pk)
134
130
  end
131
+ return nil
135
132
  end
136
133
 
137
- module ListModelMixin
138
- def index
139
- @records = self.get_filtered_data(self.get_recordset)
134
+ # Get the model for this controller.
135
+ def get_model(from_get_recordset: false)
136
+ return @model if instance_variable_defined?(:@model) && @model
137
+ return (@model = self.class.model) if self.class.model
140
138
 
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
139
+ # Delegate to the recordset's model, if it's defined.
140
+ unless from_get_recordset # prevent infinite recursion
141
+ if (recordset = self.get_recordset)
142
+ return @model = recordset.klass
149
143
  end
144
+ end
150
145
 
151
- return api_response(data)
146
+ # Try to determine model from controller name.
147
+ begin
148
+ return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
149
+ rescue NameError
152
150
  end
151
+
152
+ return nil
153
153
  end
154
154
 
155
- module ShowModelMixin
156
- def show
157
- @record = self.get_record
158
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
159
- return api_response(serialized_record)
155
+ # Get the set of records this controller has access to.
156
+ def get_recordset
157
+ return @recordset if instance_variable_defined?(:@recordset) && @recordset
158
+ return (@recordset = self.class.recordset) if self.class.recordset
159
+
160
+ # If there is a model, return that model's default scope (all records by default).
161
+ if (model = self.get_model(from_get_recordset: true))
162
+ return @recordset = model.all
160
163
  end
164
+
165
+ return nil
161
166
  end
167
+ end
162
168
 
163
- module CreateModelMixin
164
- def create
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)
169
+
170
+ # Mixin for listing records.
171
+ module RESTFramework::ListModelMixin
172
+ def index
173
+ @records = self.get_filtered_data(self.get_recordset)
174
+
175
+ # Handle pagination, if enabled.
176
+ if self.class.paginator_class
177
+ paginator = self.class.paginator_class.new(data: @records, controller: self)
178
+ page = paginator.get_page
179
+ serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
180
+ data = paginator.get_paginated_response(serialized_page)
181
+ else
182
+ data = self.get_serializer_class.new(object: @records, controller: self).serialize
174
183
  end
184
+
185
+ return api_response(data)
175
186
  end
187
+ end
176
188
 
177
- module UpdateModelMixin
178
- def update
179
- @record = self.get_record
180
- @record.update!(self.get_update_params)
181
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
182
- return api_response(serialized_record)
183
- end
189
+
190
+ # Mixin for showing records.
191
+ module RESTFramework::ShowModelMixin
192
+ def show
193
+ @record = self.get_record
194
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
195
+ return api_response(serialized_record)
184
196
  end
197
+ end
198
+
185
199
 
186
- module DestroyModelMixin
187
- def destroy
188
- @record = self.get_record
189
- @record.destroy!
190
- api_response(nil)
200
+ # Mixin for creating records.
201
+ module RESTFramework::CreateModelMixin
202
+ def create
203
+ if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
204
+ # Create with any properties inherited from the recordset (like associations).
205
+ @record = self.get_recordset.create!(self.get_create_params)
206
+ else
207
+ # Otherwise, perform a "bare" create.
208
+ @record = self.get_model.create!(self.get_create_params)
191
209
  end
210
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
211
+ return api_response(serialized_record)
192
212
  end
213
+ end
193
214
 
194
- module ReadOnlyModelControllerMixin
195
- include BaseModelControllerMixin
196
- def self.included(base)
197
- if base.is_a? Class
198
- BaseModelControllerMixin.included(base)
199
- end
200
- end
201
215
 
202
- include ListModelMixin
203
- include ShowModelMixin
216
+ # Mixin for updating records.
217
+ module RESTFramework::UpdateModelMixin
218
+ def update
219
+ @record = self.get_record
220
+ @record.update!(self.get_update_params)
221
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
222
+ return api_response(serialized_record)
223
+ end
224
+ end
225
+
226
+
227
+ # Mixin for destroying records.
228
+ module RESTFramework::DestroyModelMixin
229
+ def destroy
230
+ @record = self.get_record
231
+ @record.destroy!
232
+ api_response('')
204
233
  end
234
+ end
205
235
 
206
- module ModelControllerMixin
207
- include BaseModelControllerMixin
208
- def self.included(base)
209
- if base.is_a? Class
210
- BaseModelControllerMixin.included(base)
211
- end
236
+
237
+ # Mixin that includes show/list mixins.
238
+ module RESTFramework::ReadOnlyModelControllerMixin
239
+ include RESTFramework::BaseModelControllerMixin
240
+
241
+ def self.included(base)
242
+ if base.is_a? Class
243
+ RESTFramework::BaseModelControllerMixin.included(base)
212
244
  end
245
+ end
246
+
247
+ include RESTFramework::ListModelMixin
248
+ include RESTFramework::ShowModelMixin
249
+ end
213
250
 
214
- include ListModelMixin
215
- include ShowModelMixin
216
- include CreateModelMixin
217
- include UpdateModelMixin
218
- include DestroyModelMixin
251
+
252
+ # Mixin that includes all the CRUD mixins.
253
+ module RESTFramework::ModelControllerMixin
254
+ include RESTFramework::BaseModelControllerMixin
255
+
256
+ def self.included(base)
257
+ if base.is_a? Class
258
+ RESTFramework::BaseModelControllerMixin.included(base)
259
+ end
219
260
  end
220
261
 
262
+ include RESTFramework::ListModelMixin
263
+ include RESTFramework::ShowModelMixin
264
+ include RESTFramework::CreateModelMixin
265
+ include RESTFramework::UpdateModelMixin
266
+ include RESTFramework::DestroyModelMixin
221
267
  end