rest_framework 0.0.15 → 0.2.0

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: 70c2eb85612656cf6f1b3d34b97deb06ad01aa07459012b4254e543fc0bd5507
4
- data.tar.gz: 76f44db8c396cfe5bb539faebb6f6031276d7133327b733cab4a645922176d95
3
+ metadata.gz: 8ef7499f4d1e10af1f2520d3b9847dc7f711bd1c7f4377efe21bd5b60c0dd9a7
4
+ data.tar.gz: b15a9b0abee58e315fdc3a1157ca04d7051630821d3ed2ca691564966b5fefca
5
5
  SHA512:
6
- metadata.gz: 246bd5c3972c212c1d6fcf600da1c894fe05acaeda8596d432b6b360e265520b3e54f3b4842998c26868464c29bd79f3c164f51104145163c0a63ba9e94f06fb
7
- data.tar.gz: 52af9985b986a167f70cd8def492a3ff2c7d61bafd48e120f49934cc3827f76a1e6dbc5c02651df6bba7fdd622be058f945534415d6cb74e3537c56f9c18c0e2
6
+ metadata.gz: fb586a083989a67f206d5b5d6c14eb546949569e384d3b44c870de5f49f610b5848d53da87e01234542705304dd7d49bcf6f0037f0b89331c7fa2a1b084422ce
7
+ data.tar.gz: fe4c960cda0d2a9ba43cb7d5b1cfa84963802800829323c26b350841bbd34290e68cbbc4f14a3138a1cb95a28aafff76b2d9a78a41772950e00ad80159c8f2f3
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,24 +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
- To run unit tests:
120
-
121
- ```shell
122
- $ rake test:unit
123
- ```
124
-
125
- To run integration tests:
126
-
127
- ```shell
128
- $ rake test:integration
129
- ```
130
-
131
- To interact with the integration app, you can `cd test/integration` and operate it via the normal
132
- Rails interfaces. Ensure you run `rake db:schema:load` before running `rails server` or
133
- `rails console`.
125
+ To interact with the test app, `cd test` and operate it via the normal Rails interfaces. Ensure you
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`.
@@ -30,14 +30,14 @@
30
30
  <% if @json_payload %>
31
31
  <li class="nav-item">
32
32
  <a class="nav-link active" href="#tab-json" data-toggle="tab" role="tab">
33
- JSON
33
+ .json
34
34
  </a>
35
35
  </li>
36
36
  <% end %>
37
37
  <% if @xml_payload %>
38
38
  <li class="nav-item">
39
39
  <a class="nav-link" href="#tab-xml" data-toggle="tab" role="tab">
40
- XML
40
+ .xml
41
41
  </a>
42
42
  </li>
43
43
  <% end %>
@@ -1,7 +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"
8
+ require_relative "rest_framework/filters"
9
+ require_relative "rest_framework/paginators"
6
10
  require_relative "rest_framework/routers"
11
+ require_relative "rest_framework/serializers"
7
12
  require_relative "rest_framework/version"
13
+ require_relative "rest_framework/generators"
@@ -1 +1 @@
1
- 0.0.15
1
+ 0.2.0
@@ -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,134 +1,189 @@
1
- module RESTFramework
2
-
3
- # This module provides the common functionality for any controller mixins, a `root` action, and
4
- # the ability to route arbitrary actions with `extra_actions`. This is also where `api_response`
5
- # is defined.
6
- module BaseControllerMixin
7
- # Default action for API root.
8
- def root
9
- api_response({message: "This is the root of your awesome API!"})
10
- end
1
+ require_relative '../errors'
2
+ require_relative '../serializers'
11
3
 
12
- module ClassMethods
13
- def get_skip_actions(skip_undefined: true)
14
- # first, skip explicitly skipped actions
15
- skip = self.skip_actions || []
16
4
 
17
- # now add methods which don't exist, since we don't want to route those
18
- if skip_undefined
19
- [:index, :new, :create, :show, :edit, :update, :destroy].each do |a|
20
- skip << a unless self.method_defined?(a)
21
- end
22
- 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
23
13
 
24
- return skip
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 || []
19
+
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
+ end
25
25
  end
26
+
27
+ return skip
26
28
  end
29
+ end
27
30
 
28
- def self.included(base)
29
- if base.is_a? Class
30
- base.extend ClassMethods
31
- base.class_attribute(*[
32
- :singleton_controller,
33
- :extra_actions,
34
- :skip_actions,
35
- :paginator_class,
36
- ])
37
-
38
- # skip csrf since this is an API
39
- base.skip_before_action(:verify_authenticity_token) rescue nil
40
-
41
- # handle some common exceptions
42
- base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
43
- base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
44
- base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
45
- base.rescue_from(ActiveRecord::RecordNotDestroyed, with: :record_not_destroyed)
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)
60
+ end
46
61
  end
47
- end
48
62
 
49
- protected
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=)
67
+ end
50
68
 
51
- def record_invalid(e)
52
- return api_response(
53
- {message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
54
- )
55
- end
69
+ # Skip csrf since this is an API.
70
+ base.skip_before_action(:verify_authenticity_token) rescue nil
56
71
 
57
- def record_not_found(e)
58
- return api_response({message: "Record not found.", exception: e}, status: 404)
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)
59
77
  end
78
+ end
60
79
 
61
- def record_not_saved(e)
62
- return api_response({message: "Record not saved.", exception: e}, status: 406)
63
- end
80
+ protected
64
81
 
65
- def record_not_destroyed(e)
66
- return api_response({message: "Record not destroyed.", exception: e}, status: 406)
67
- 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
68
87
 
69
- def _get_routes
70
- begin
71
- formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
72
- rescue NameError
73
- formatter = ActionDispatch::Routing::ConsoleFormatter
74
- end
75
- return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
76
- formatter.new
77
- ).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
78
- {verb: r[0], path: r[1], action: r[2]}
79
- }.select { |r| r[:path].start_with?(request.path) }
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)
80
93
  end
81
94
 
82
- # Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
83
- # primitives.
84
- def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
85
- html_kwargs ||= {}
86
- json_kwargs ||= {}
87
- xml_kwargs ||= {}
95
+ return data
96
+ end
88
97
 
89
- # allow blank (no-content) responses
90
- @blank = kwargs[:blank]
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
91
103
 
92
- respond_to do |format|
93
- if @blank
94
- format.json {head :no_content}
95
- format.xml {head :no_content}
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
+
110
+ def record_not_found(e)
111
+ return api_response({message: "Record not found.", exception: e}, status: 404)
112
+ end
113
+
114
+ def record_not_saved(e)
115
+ return api_response({message: "Record not saved.", exception: e}, status: 406)
116
+ end
117
+
118
+ def record_not_destroyed(e)
119
+ return api_response({message: "Record not destroyed.", exception: e}, status: 406)
120
+ end
121
+
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
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
135
+
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
149
+ end
150
+
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
96
171
  else
97
- if payload.respond_to?(:to_json)
98
- format.json {
99
- kwargs = kwargs.merge(json_kwargs)
100
- render(json: payload, layout: false, **kwargs)
101
- }
102
- end
103
- if payload.respond_to?(:to_xml)
104
- format.xml {
105
- kwargs = kwargs.merge(xml_kwargs)
106
- render(xml: payload, layout: false, **kwargs)
107
- }
108
- end
172
+ @json_payload = payload.to_json if self.serialize_to_json
173
+ @xml_payload = payload.to_xml if self.serialize_to_xml
109
174
  end
110
- format.html {
111
- @payload = payload
112
- @json_payload = ''
113
- @xml_payload = ''
114
- unless @blank
115
- @json_payload = payload.to_json if payload.respond_to?(:to_json)
116
- @xml_payload = payload.to_xml if payload.respond_to?(:to_xml)
117
- end
118
- @template_logo_text ||= "Rails REST Framework"
119
- @title ||= self.controller_name.camelize
120
- @routes ||= self._get_routes
121
- kwargs = kwargs.merge(html_kwargs)
122
- begin
123
- render(**kwargs)
124
- rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
125
- kwargs[:layout] = "rest_framework"
126
- kwargs[:template] = "rest_framework/default"
127
- end
128
- render(**kwargs)
129
- }
130
- 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
+ }
131
187
  end
132
188
  end
133
-
134
189
  end
@@ -1,229 +1,267 @@
1
1
  require_relative 'base'
2
- require_relative '../serializers'
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
- base.class_attribute(*[
12
- :model,
13
- :recordset,
14
- :fields,
15
- :action_fields,
16
- :native_serializer_config,
17
- :native_serializer_action_config,
18
- :filterset_fields,
19
- :allowed_parameters,
20
- :allowed_action_parameters,
21
- :serializer_class,
22
- :extra_member_actions,
23
- :disable_creation_from_recordset,
24
- ])
25
- base.alias_method(:extra_collection_actions=, :extra_actions=)
26
- end
27
- end
2
+ require_relative '../filters'
28
3
 
29
- protected
30
4
 
31
- def get_serializer_class
32
- return self.class.serializer_class || NativeModelSerializer
33
- end
5
+ # This module provides the core functionality for controllers based on models.
6
+ module RESTFramework::BaseModelControllerMixin
7
+ include RESTFramework::BaseControllerMixin
34
8
 
35
- # Get a list of fields for the current action.
36
- def get_fields
37
- action_fields = self.class.action_fields || {}
38
- action = self.action_name.to_sym
9
+ def self.included(base)
10
+ if base.is_a? Class
11
+ RESTFramework::BaseControllerMixin.included(base)
39
12
 
40
- # index action should use :list fields if :index is not provided
41
- action = :list if action == :index && !action_fields.key?(:index)
13
+ # Add class attributes (with defaults) unless they already exist.
14
+ {
15
+ # Core attributes related to models.
16
+ model: nil,
17
+ recordset: nil,
42
18
 
43
- return (action_fields[action] if action) || self.class.fields || []
44
- end
19
+ # Attributes for configuring record fields.
20
+ fields: nil,
21
+ action_fields: nil,
45
22
 
46
- # Get a native serializer config for the current action.
47
- def get_native_serializer_config
48
- action_serializer_config = self.class.native_serializer_action_config || {}
49
- action = self.action_name.to_sym
23
+ # Attributes for create/update parameters.
24
+ allowed_parameters: nil,
25
+ allowed_action_parameters: nil,
50
26
 
51
- # index action should use :list serializer config if :index is not provided
52
- action = :list if action == :index && !action_serializer_config.key?(:index)
27
+ # Attributes for the default native serializer.
28
+ native_serializer_config: nil,
29
+ native_serializer_singular_config: nil,
30
+ native_serializer_plural_config: nil,
53
31
 
54
- return (action_serializer_config[action] if action) || self.class.native_serializer_config
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)
46
+ end
47
+ end
55
48
  end
49
+ end
56
50
 
57
- # Get a list of parameters allowed for the current action.
58
- def get_allowed_parameters
59
- allowed_action_parameters = self.class.allowed_action_parameters || {}
60
- action = self.action_name.to_sym
51
+ protected
61
52
 
62
- # index action should use :list allowed parameters if :index is not provided
63
- action = :list if action == :index && !allowed_action_parameters.key?(:index)
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
64
56
 
65
- return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
66
- end
57
+ # Index action should use :list serializer if :index is not provided.
58
+ action = :list if action == :index && !action_config.key?(:index)
59
+
60
+ return (action_config[action] if action) || self.class.send(generic_config_key)
61
+ end
62
+
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
67
+
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
67
76
 
68
- # Filter the request body for keys in current action's allowed_parameters/fields config.
69
- def _get_parameter_values_from_request_body
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
82
+
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
90
+
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
70
94
  fields = self.get_allowed_parameters || self.get_fields
71
- return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
72
- fields.include?(p.to_sym) || fields.include?(p.to_s)
73
- })
74
- end
75
- alias :get_create_params :_get_parameter_values_from_request_body
76
- alias :get_update_params :_get_parameter_values_from_request_body
77
-
78
- # Filter params for keys allowed by the current action's filterset_fields/fields config.
79
- def _get_filterset_values_from_params
80
- fields = self.filterset_fields || self.get_fields
81
- return @_get_filterset_values_from_params ||= request.query_parameters.select { |p|
82
- fields.include?(p.to_sym) || fields.include?(p.to_s)
95
+
96
+ # Filter the request body.
97
+ body_params = request.request_parameters.select { |p|
98
+ fields.include?(p)
83
99
  }
84
- end
85
- alias :get_lookup_params :_get_filterset_values_from_params
86
- alias :get_filter_params :_get_filterset_values_from_params
87
-
88
- # Get the recordset, filtered by the filter params.
89
- def get_filtered_recordset
90
- filter_params = self.get_filter_params
91
- unless filter_params.blank?
92
- return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
93
- end
94
- return self.get_recordset
95
- end
96
100
 
97
- # Get a record by `id` or return a single record if recordset is filtered down to a single
98
- # record.
99
- def get_record
100
- records = self.get_filtered_recordset
101
- if params['id'] # direct lookup
102
- return records.find(params['id'])
103
- elsif records.length == 1
104
- return records[0]
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
105
108
  end
106
- return nil
107
- end
108
109
 
109
- # Internal interface for get_model, protecting against infinite recursion with get_recordset.
110
- def _get_model(from_internal_get_recordset: false)
111
- return @model if instance_variable_defined?(:@model) && @model
112
- return self.class.model if self.class.model
113
- unless from_internal_get_recordset # prevent infinite recursion
114
- recordset = self._get_recordset(from_internal_get_model: true)
115
- return (@model = recordset.klass) if recordset
110
+ # Filter primary key if configured.
111
+ if self.class.filter_pk_from_request_body
112
+ body_params.delete(self.get_model&.primary_key)
116
113
  end
117
- begin
118
- return (@model = self.class.name.demodulize.match(/(.*)Controller/)[1].singularize.constantize)
119
- rescue NameError
114
+
115
+ # Filter fields in exclude_body_fields.
116
+ (self.class.exclude_body_fields || []).each do |f|
117
+ body_params.delete(f.to_s)
120
118
  end
121
- return nil
119
+
120
+ body_params
122
121
  end
122
+ end
123
+ alias :get_create_params :get_body_params
124
+ alias :get_update_params :get_body_params
123
125
 
124
- # Internal interface for get_recordset, protecting against infinite recursion with get_model.
125
- def _get_recordset(from_internal_get_model: false)
126
- return @recordset if instance_variable_defined?(:@recordset) && @recordset
127
- return self.class.recordset if self.class.recordset
128
- unless from_internal_get_model # prevent infinite recursion
129
- model = self._get_model(from_internal_get_recordset: true)
130
- return (@recordset = model.all) if model
131
- end
132
- return nil
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)
133
130
  end
131
+ return nil
132
+ end
133
+
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
134
138
 
135
- # Get the model for this controller.
136
- def get_model
137
- return _get_model
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
143
+ end
138
144
  end
139
145
 
140
- # Get the set of records this controller has access to.
141
- def get_recordset
142
- return _get_recordset
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
143
150
  end
151
+
152
+ return nil
144
153
  end
145
154
 
146
- module ListModelMixin
147
- # TODO: pagination classes like Django
148
- def index
149
- @records = self.get_filtered_recordset
150
- @serialized_records = self.get_serializer_class.new(
151
- object: @records, controller: self
152
- ).serialize
153
- return api_response(@serialized_records)
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
154
163
  end
164
+
165
+ return nil
155
166
  end
167
+ end
168
+
156
169
 
157
- module ShowModelMixin
158
- def show
159
- @record = self.get_record
160
- @serialized_record = self.get_serializer_class.new(
161
- object: @record, controller: self
162
- ).serialize
163
- return api_response(@serialized_record)
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
164
183
  end
184
+
185
+ return api_response(data)
165
186
  end
187
+ end
166
188
 
167
- module CreateModelMixin
168
- def create
169
- if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
170
- # Create with any properties inherited from the recordset (like associations).
171
- @record = self.get_recordset.create!(self.get_create_params)
172
- else
173
- # Otherwise, perform a "bare" create.
174
- @record = self.get_model.create!(self.get_create_params)
175
- end
176
- @serialized_record = self.get_serializer_class.new(
177
- object: @record, controller: self
178
- ).serialize
179
- return api_response(@serialized_record)
180
- 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)
181
196
  end
197
+ end
198
+
182
199
 
183
- module UpdateModelMixin
184
- def update
185
- @record = self.get_record
186
- @record.update!(self.get_update_params)
187
- @serialized_record = self.get_serializer_class.new(
188
- object: @record, controller: self
189
- ).serialize
190
- return api_response(@serialized_record)
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 DestroyModelMixin
195
- def destroy
196
- @record = self.get_record
197
- @record.destroy!
198
- api_response(nil)
199
- end
215
+
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)
200
223
  end
224
+ end
201
225
 
202
- module ReadOnlyModelControllerMixin
203
- include BaseModelControllerMixin
204
- def self.included(base)
205
- if base.is_a? Class
206
- BaseModelControllerMixin.included(base)
207
- end
208
- end
209
226
 
210
- include ListModelMixin
211
- include ShowModelMixin
227
+ # Mixin for destroying records.
228
+ module RESTFramework::DestroyModelMixin
229
+ def destroy
230
+ @record = self.get_record
231
+ @record.destroy!
232
+ api_response('')
212
233
  end
234
+ end
213
235
 
214
- module ModelControllerMixin
215
- include BaseModelControllerMixin
216
- def self.included(base)
217
- if base.is_a? Class
218
- BaseModelControllerMixin.included(base)
219
- 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)
220
244
  end
245
+ end
246
+
247
+ include RESTFramework::ListModelMixin
248
+ include RESTFramework::ShowModelMixin
249
+ end
221
250
 
222
- include ListModelMixin
223
- include ShowModelMixin
224
- include CreateModelMixin
225
- include UpdateModelMixin
226
- 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
227
260
  end
228
261
 
262
+ include RESTFramework::ListModelMixin
263
+ include RESTFramework::ShowModelMixin
264
+ include RESTFramework::CreateModelMixin
265
+ include RESTFramework::UpdateModelMixin
266
+ include RESTFramework::DestroyModelMixin
229
267
  end