rest_framework 0.0.16 → 0.2.1

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