rest_framework 0.1.0 → 0.2.2

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