rest_framework 0.0.15 → 0.2.0

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