rest_framework 0.1.0 → 0.2.2

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: 46db4e7aa05600caaa4c1f9f645f417b4d3cd9e5bcde7da0b54d3c5fa84e872e
4
- data.tar.gz: 95c9d018b687795f80c38b43289f19ffd44eb1b170d883c164b590ab6b1c20d0
3
+ metadata.gz: 37a40fca2be23a6fe5fb1f982349ce5dd212644ffe450d979d04c339360b22eb
4
+ data.tar.gz: 3e036f7150018a46756efc378dacf16321a9394cb9d04d9b4af7220158400cda
5
5
  SHA512:
6
- metadata.gz: 2c376d191ffa5ae9de932dceb8411362a00be789ff9aa3733f038608da890fc5437d981576e3b17d402b857f7966cb15e437d7cbe2c8447191088a9a2249134f
7
- data.tar.gz: 72e31acb2e66c6d8d2af2dd7375e7732a93b5e66b1c1fedf4c1d781421422c582958d32c6022305cd9cc2ca9dda51e2c2f6c540b1737e11eb3506501ef91b618
6
+ metadata.gz: 9c4a665ccde25d1c53423c168d1e5849d4ba4b91a2a0bcf6fb1f3b3714b624112d951c06aa84029aeaeb7c3703b30e8c73824688070370b3f0e9e3c3d2fd70ec
7
+ data.tar.gz: 6a4511553767ed27726a9d12daf0cd46958fe2098566f0daff5c1f9056bfbfa8c26836646b76e3f4a22a1efbea341dc6cb682a04c7c1b5b7635b46cc56548c3a
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,7 +116,7 @@ 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
@@ -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.1.0
1
+ 0.2.2
@@ -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,194 +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
- paginator_class: nil,
40
- page_size: nil,
41
- page_query_param: 'page',
42
- page_size_query_param: 'page_size',
43
- max_page_size: nil,
44
- serializer_class: nil,
45
- singleton_controller: nil,
46
- skip_actions: nil,
47
- }.each do |a, default|
48
- unless base.respond_to?(a)
49
- base.class_attribute(a)
50
-
51
- # Set default manually so we can still support Rails 4. Maybe later we can use the
52
- # default parameter on `class_attribute`.
53
- base.send(:"#{a}=", default)
54
- end
55
- end
27
+ return skip
28
+ end
29
+ end
56
30
 
57
- # Alias `extra_actions` to `extra_collection_actions`.
58
- unless base.respond_to?(:extra_collection_actions)
59
- base.alias_method(:extra_collection_actions, :extra_actions)
60
- base.alias_method(:extra_collection_actions=, :extra_actions=)
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)
61
60
  end
61
+ end
62
62
 
63
- # skip csrf since this is an API
64
- base.skip_before_action(:verify_authenticity_token) rescue nil
65
-
66
- # handle some common exceptions
67
- base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
68
- base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
69
- base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
70
- base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
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=)
71
67
  end
72
- end
73
68
 
74
- protected
69
+ # Skip csrf since this is an API.
70
+ base.skip_before_action(:verify_authenticity_token) rescue nil
75
71
 
76
- # Helper to get filtering backends with a sane default.
77
- def get_filter_backends
78
- return self.class.filter_backends || []
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)
79
77
  end
78
+ end
80
79
 
81
- # Filter the recordset over all configured filter backends.
82
- def get_filtered_data(data)
83
- self.get_filter_backends.each do |filter_class|
84
- filter = filter_class.new(controller: self)
85
- data = filter.get_filtered_data(data)
86
- end
80
+ protected
87
81
 
88
- return data
89
- 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
90
87
 
91
- # Helper to get the configured serializer class.
92
- def get_serializer_class
93
- return self.class.serializer_class
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)
94
93
  end
95
94
 
96
- # Get a native serializer config for the current action.
97
- def get_native_serializer_config
98
- action_serializer_config = self.class.native_serializer_action_config || {}
99
- action = self.action_name.to_sym
95
+ return data
96
+ end
100
97
 
101
- # Handle case where :index action is not defined.
102
- if action == :index && !action_serializer_config.key?(:index)
103
- # Default is :show if `singleton_controller`, otherwise :list.
104
- action = self.class.singleton_controller ? :show : :list
105
- end
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
106
103
 
107
- return (action_serializer_config[action] if action) || self.class.native_serializer_config
108
- 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
109
109
 
110
- def record_invalid(e)
111
- return api_response(
112
- {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
113
- )
114
- end
110
+ def record_not_found(e)
111
+ return api_response({message: "Record not found.", exception: e}, status: 404)
112
+ end
115
113
 
116
- def record_not_found(e)
117
- return api_response({message: "Record not found.", exception: e}, status: 404)
118
- end
114
+ def record_not_saved(e)
115
+ return api_response({message: "Record not saved.", exception: e}, status: 406)
116
+ end
119
117
 
120
- def record_not_saved(e)
121
- return api_response({message: "Record not saved.", exception: e}, status: 406)
122
- end
118
+ def record_not_destroyed(e)
119
+ return api_response({message: "Record not destroyed.", exception: e}, status: 406)
120
+ end
123
121
 
124
- def record_not_destroyed(e)
125
- 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
126
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
127
135
 
128
- # Helper for showing routes under a controller action, used for the browsable API.
129
- def _get_routes
130
- begin
131
- formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
132
- rescue NameError
133
- formatter = ActionDispatch::Routing::ConsoleFormatter
134
- end
135
- return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
136
- formatter.new
137
- ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
138
- {verb: r[0], path: r[1], action: r[2]}
139
- }.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
140
149
  end
141
150
 
142
- # Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
143
- # primitives.
144
- def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
145
- html_kwargs ||= {}
146
- json_kwargs ||= {}
147
- xml_kwargs ||= {}
148
-
149
- # allow blank (no-content) responses
150
- @blank = kwargs[:blank]
151
-
152
- respond_to do |format|
153
- if @blank
154
- format.json {head :no_content}
155
- 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
156
171
  else
157
- if payload.respond_to?(:to_json)
158
- format.json {
159
- kwargs = kwargs.merge(json_kwargs)
160
- render(json: payload, layout: false, **kwargs)
161
- }
162
- end
163
- if payload.respond_to?(:to_xml)
164
- format.xml {
165
- kwargs = kwargs.merge(xml_kwargs)
166
- render(xml: payload, layout: false, **kwargs)
167
- }
168
- end
172
+ @json_payload = payload.to_json if self.serialize_to_json
173
+ @xml_payload = payload.to_xml if self.serialize_to_xml
169
174
  end
170
- format.html {
171
- @payload = payload
172
- @json_payload = ''
173
- @xml_payload = ''
174
- unless @blank
175
- @json_payload = payload.to_json if payload.respond_to?(:to_json)
176
- @xml_payload = payload.to_xml if payload.respond_to?(:to_xml)
177
- end
178
- @template_logo_text ||= "Rails REST Framework"
179
- @title ||= self.controller_name.camelize
180
- @routes ||= self._get_routes
181
- kwargs = kwargs.merge(html_kwargs)
182
- begin
183
- render(**kwargs)
184
- rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
185
- kwargs[:layout] = "rest_framework"
186
- kwargs[:template] = "rest_framework/default"
187
- render(**kwargs)
188
- end
189
- }
190
- 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
+ }
191
187
  end
192
188
  end
193
-
194
189
  end
@@ -1,228 +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 the default native serializer.
27
- native_serializer_config: nil,
28
- native_serializer_action_config: nil,
29
-
30
- # Attributes for default model filtering (and ordering).
31
- filterset_fields: nil,
32
- ordering_fields: nil,
33
- ordering_query_param: 'ordering',
34
-
35
- # Other misc attributes.
36
- disable_creation_from_recordset: nil, # Option to disable `recordset.create` behavior.
37
- }.each do |a, default|
38
- unless base.respond_to?(a)
39
- base.class_attribute(a)
40
-
41
- # Set default manually so we can still support Rails 4. Maybe later we can use the
42
- # default parameter on `class_attribute`.
43
- base.send(:"#{a}=", default)
44
- end
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)
45
46
  end
46
47
  end
47
48
  end
49
+ end
48
50
 
49
- protected
51
+ protected
50
52
 
51
- # Helper to get the configured serializer class, or `NativeModelSerializer` as a default.
52
- def get_serializer_class
53
- return self.class.serializer_class || RESTFramework::NativeModelSerializer
54
- end
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
55
56
 
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 || {}
59
- action = self.action_name.to_sym
57
+ # Index action should use :list serializer if :index is not provided.
58
+ action = :list if action == :index && !action_config.key?(:index)
60
59
 
61
- # index action should use :list allowed parameters if :index is not provided
62
- action = :list if action == :index && !allowed_action_parameters.key?(:index)
63
-
64
- return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
65
- end
60
+ return (action_config[action] if action) || self.class.send(generic_config_key)
61
+ end
66
62
 
67
- # Get the list of filtering backends to use.
68
- def get_filter_backends
69
- return self.class.filter_backends || [
70
- RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
71
- ]
72
- 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
73
67
 
74
- # Get a list of fields for the current action.
75
- def get_fields
76
- action_fields = self.class.action_fields || {}
77
- 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
78
76
 
79
- # index action should use :list fields if :index is not provided
80
- 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
81
82
 
82
- return (action_fields[action] if action) || self.class.fields || []
83
- 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
84
90
 
85
- # Filter the request body for keys in current action's allowed_parameters/fields config.
86
- def get_body_params
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
87
94
  fields = self.get_allowed_parameters || self.get_fields
88
- return @get_body_params ||= (request.request_parameters.select { |p|
89
- fields.include?(p.to_sym) || fields.include?(p.to_s)
90
- })
91
- end
92
- alias :get_create_params :get_body_params
93
- alias :get_update_params :get_body_params
94
-
95
- # Get a record by `id` or return a single record if recordset is filtered down to a single
96
- # record.
97
- def get_record
98
- records = self.get_filtered_data(self.get_recordset)
99
- if params['id'] # direct lookup
100
- return records.find(params['id'])
101
- elsif records.length == 1
102
- return records[0]
103
- end
104
- return nil
105
- end
106
95
 
107
- # Internal interface for get_model, protecting against infinite recursion with get_recordset.
108
- def _get_model(from_internal_get_recordset: false)
109
- return @model if instance_variable_defined?(:@model) && @model
110
- return self.class.model if self.class.model
111
- unless from_internal_get_recordset # prevent infinite recursion
112
- recordset = self._get_recordset(from_internal_get_model: true)
113
- 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
114
108
  end
115
- begin
116
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
117
- 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)
118
113
  end
119
- return nil
120
- end
121
114
 
122
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
123
- def _get_recordset(from_internal_get_model: false)
124
- return @recordset if instance_variable_defined?(:@recordset) && @recordset
125
- return self.class.recordset if self.class.recordset
126
- unless from_internal_get_model # prevent infinite recursion
127
- model = self._get_model(from_internal_get_recordset: true)
128
- 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)
129
118
  end
130
- return nil
131
- end
132
119
 
133
- # Get the model for this controller.
134
- def get_model
135
- return _get_model
120
+ body_params
136
121
  end
122
+ end
123
+ alias :get_create_params :get_body_params
124
+ alias :get_update_params :get_body_params
137
125
 
138
- # Get the set of records this controller has access to.
139
- def get_recordset
140
- 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)
141
130
  end
131
+ return nil
142
132
  end
143
133
 
144
- module ListModelMixin
145
- def index
146
- @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
147
138
 
148
- # Handle pagination, if enabled.
149
- if self.class.paginator_class
150
- paginator = self.class.paginator_class.new(data: @records, controller: self)
151
- page = paginator.get_page
152
- serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
153
- data = paginator.get_paginated_response(serialized_page)
154
- else
155
- data = self.get_serializer_class.new(object: @records, controller: self).serialize
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
156
143
  end
144
+ end
157
145
 
158
- 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
159
150
  end
151
+
152
+ return nil
160
153
  end
161
154
 
162
- module ShowModelMixin
163
- def show
164
- @record = self.get_record
165
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
166
- 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
167
163
  end
164
+
165
+ return nil
168
166
  end
167
+ end
169
168
 
170
- module CreateModelMixin
171
- def create
172
- if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
173
- # Create with any properties inherited from the recordset (like associations).
174
- @record = self.get_recordset.create!(self.get_create_params)
175
- else
176
- # Otherwise, perform a "bare" create.
177
- @record = self.get_model.create!(self.get_create_params)
178
- end
179
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
180
- return api_response(serialized_record)
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
181
183
  end
184
+
185
+ return api_response(data)
182
186
  end
187
+ end
183
188
 
184
- module UpdateModelMixin
185
- def update
186
- @record = self.get_record
187
- @record.update!(self.get_update_params)
188
- serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
189
- return api_response(serialized_record)
190
- 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)
191
196
  end
197
+ end
192
198
 
193
- module DestroyModelMixin
194
- def destroy
195
- @record = self.get_record
196
- @record.destroy!
197
- api_response(nil)
199
+
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)
198
209
  end
210
+ serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
211
+ return api_response(serialized_record)
199
212
  end
213
+ end
200
214
 
201
- module ReadOnlyModelControllerMixin
202
- include BaseModelControllerMixin
203
- def self.included(base)
204
- if base.is_a? Class
205
- BaseModelControllerMixin.included(base)
206
- end
207
- end
208
215
 
209
- include ListModelMixin
210
- 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)
211
223
  end
224
+ end
212
225
 
213
- module ModelControllerMixin
214
- include BaseModelControllerMixin
215
- def self.included(base)
216
- if base.is_a? Class
217
- BaseModelControllerMixin.included(base)
218
- end
226
+
227
+ # Mixin for destroying records.
228
+ module RESTFramework::DestroyModelMixin
229
+ def destroy
230
+ @record = self.get_record
231
+ @record.destroy!
232
+ api_response('')
233
+ end
234
+ end
235
+
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)
219
244
  end
245
+ end
246
+
247
+ include RESTFramework::ListModelMixin
248
+ include RESTFramework::ShowModelMixin
249
+ end
220
250
 
221
- include ListModelMixin
222
- include ShowModelMixin
223
- include CreateModelMixin
224
- include UpdateModelMixin
225
- 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
226
260
  end
227
261
 
262
+ include RESTFramework::ListModelMixin
263
+ include RESTFramework::ShowModelMixin
264
+ include RESTFramework::CreateModelMixin
265
+ include RESTFramework::UpdateModelMixin
266
+ include RESTFramework::DestroyModelMixin
228
267
  end