rest_framework 0.0.10 → 0.0.16
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +2 -15
- data/app/views/layouts/rest_framework.html.erb +3 -3
- data/lib/rest_framework.rb +3 -0
- data/lib/rest_framework/VERSION_STAMP +1 -1
- data/lib/rest_framework/controller_mixins/base.rb +106 -27
- data/lib/rest_framework/controller_mixins/models.rb +78 -79
- data/lib/rest_framework/filters.rb +65 -0
- data/lib/rest_framework/paginators.rb +84 -0
- data/lib/rest_framework/serializers.rb +62 -58
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 825e7e7ac0e9c8ae57250288a54dc3ef6510665ce3d4fee23245c559aa8e8233
|
4
|
+
data.tar.gz: 2211c303d14708ef94a4cb35d6e3b25bf78b3b08645f2990ca5448354d8dd64e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ee2cf406f2b8d77d9ea63650b080d037a759143e0a3e19846585d493a386a4647db0fe59188aa0fd887257a18087aaca82b80d99dee962af1dfdb7eca66a07c
|
7
|
+
data.tar.gz: 6248caea5ad7f5a37a55f7da6aae55c70dc284b9f04d046fc6149a13dff4ae34410106c97f54274f59a8353c79fe97073b7dcba51d65d9c94eb0f9cff1a205ac
|
data/README.md
CHANGED
@@ -116,18 +116,5 @@ To run the full test suite:
|
|
116
116
|
$ rake test
|
117
117
|
```
|
118
118
|
|
119
|
-
To
|
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`.
|
119
|
+
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`.
|
@@ -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
|
-
|
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
|
-
|
40
|
+
.xml
|
41
41
|
</a>
|
42
42
|
</li>
|
43
43
|
<% end %>
|
@@ -46,7 +46,7 @@
|
|
46
46
|
<div class="tab-content w-100 pt-3">
|
47
47
|
<div class="tab-pane fade show active" id="tab-json" role="tab">
|
48
48
|
<% if @json_payload %>
|
49
|
-
<div><pre><code class="language-json"><%= JSON.pretty_generate(JSON.parse(@json_payload)) %></code></pre></div>
|
49
|
+
<div><pre><code class="language-json"><%= JSON.pretty_generate(JSON.parse(@json_payload)) unless @json_payload == '' %></code></pre></div>
|
50
50
|
<% end %>
|
51
51
|
</div>
|
52
52
|
<div class="tab-pane fade" id="tab-xml" role="tab">
|
data/lib/rest_framework.rb
CHANGED
@@ -3,5 +3,8 @@ end
|
|
3
3
|
|
4
4
|
require_relative "rest_framework/controller_mixins"
|
5
5
|
require_relative "rest_framework/engine"
|
6
|
+
require_relative "rest_framework/filters"
|
7
|
+
require_relative "rest_framework/paginators"
|
6
8
|
require_relative "rest_framework/routers"
|
9
|
+
require_relative "rest_framework/serializers"
|
7
10
|
require_relative "rest_framework/version"
|
@@ -1 +1 @@
|
|
1
|
-
0.0.
|
1
|
+
0.0.16
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require_relative '../serializers'
|
2
|
+
|
1
3
|
module RESTFramework
|
2
4
|
|
3
5
|
# This module provides the common functionality for any controller mixins, a `root` action, and
|
@@ -28,12 +30,42 @@ module RESTFramework
|
|
28
30
|
def self.included(base)
|
29
31
|
if base.is_a? Class
|
30
32
|
base.extend ClassMethods
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
:
|
35
|
-
:
|
36
|
-
|
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
|
58
|
+
|
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=)
|
63
|
+
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
|
37
69
|
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
38
70
|
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
39
71
|
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
@@ -43,6 +75,45 @@ module RESTFramework
|
|
43
75
|
|
44
76
|
protected
|
45
77
|
|
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
|
82
|
+
end
|
83
|
+
|
84
|
+
# By default, return nil.
|
85
|
+
return nil
|
86
|
+
end
|
87
|
+
|
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
|
94
|
+
|
95
|
+
return data
|
96
|
+
end
|
97
|
+
|
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
|
101
|
+
end
|
102
|
+
|
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
|
107
|
+
|
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
|
113
|
+
|
114
|
+
return (action_serializer_config[action] if action) || self.class.native_serializer_config
|
115
|
+
end
|
116
|
+
|
46
117
|
def record_invalid(e)
|
47
118
|
return api_response(
|
48
119
|
{message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
|
@@ -61,6 +132,7 @@ module RESTFramework
|
|
61
132
|
return api_response({message: "Record not destroyed.", exception: e}, status: 406)
|
62
133
|
end
|
63
134
|
|
135
|
+
# Helper for showing routes under a controller action, used for the browsable API.
|
64
136
|
def _get_routes
|
65
137
|
begin
|
66
138
|
formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
|
@@ -69,40 +141,47 @@ module RESTFramework
|
|
69
141
|
end
|
70
142
|
return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
|
71
143
|
formatter.new
|
72
|
-
).lines
|
144
|
+
).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
|
73
145
|
{verb: r[0], path: r[1], action: r[2]}
|
74
146
|
}.select { |r| r[:path].start_with?(request.path) }
|
75
147
|
end
|
76
148
|
|
77
|
-
# Helper alias for `respond_to`/`render
|
78
|
-
#
|
149
|
+
# Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
|
150
|
+
# primitives.
|
79
151
|
def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
|
80
152
|
html_kwargs ||= {}
|
81
153
|
json_kwargs ||= {}
|
82
154
|
xml_kwargs ||= {}
|
83
155
|
|
84
|
-
#
|
85
|
-
|
86
|
-
kwargs[:status] = 204
|
87
|
-
end
|
156
|
+
# allow blank (no-content) responses
|
157
|
+
@blank = kwargs[:blank]
|
88
158
|
|
89
159
|
respond_to do |format|
|
90
|
-
if
|
91
|
-
format.json {
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
160
|
+
if @blank
|
161
|
+
format.json {head :no_content}
|
162
|
+
format.xml {head :no_content}
|
163
|
+
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
|
101
176
|
end
|
102
177
|
format.html {
|
103
178
|
@payload = payload
|
104
|
-
@json_payload =
|
105
|
-
@xml_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
|
106
185
|
@template_logo_text ||= "Rails REST Framework"
|
107
186
|
@title ||= self.controller_name.camelize
|
108
187
|
@routes ||= self._get_routes
|
@@ -112,8 +191,8 @@ module RESTFramework
|
|
112
191
|
rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
|
113
192
|
kwargs[:layout] = "rest_framework"
|
114
193
|
kwargs[:template] = "rest_framework/default"
|
194
|
+
render(**kwargs)
|
115
195
|
end
|
116
|
-
render(**kwargs)
|
117
196
|
}
|
118
197
|
end
|
119
198
|
end
|
@@ -1,5 +1,5 @@
|
|
1
1
|
require_relative 'base'
|
2
|
-
require_relative '../
|
2
|
+
require_relative '../filters'
|
3
3
|
|
4
4
|
module RESTFramework
|
5
5
|
|
@@ -8,95 +8,87 @@ module RESTFramework
|
|
8
8
|
def self.included(base)
|
9
9
|
if base.is_a? Class
|
10
10
|
BaseControllerMixin.included(base)
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
:
|
16
|
-
:
|
17
|
-
|
18
|
-
|
19
|
-
:
|
20
|
-
:
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
41
|
+
end
|
25
42
|
end
|
26
43
|
end
|
27
44
|
|
28
45
|
protected
|
29
46
|
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
# Get a list of fields for the current action.
|
35
|
-
def get_fields
|
36
|
-
action_fields = self.class.action_fields || {}
|
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 || {}
|
37
50
|
action = self.action_name.to_sym
|
38
51
|
|
39
|
-
# index action should use :list
|
40
|
-
action = :list if action == :index && !
|
52
|
+
# index action should use :list allowed parameters if :index is not provided
|
53
|
+
action = :list if action == :index && !allowed_action_parameters.key?(:index)
|
41
54
|
|
42
|
-
return (
|
55
|
+
return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
|
43
56
|
end
|
44
57
|
|
45
|
-
# Get
|
46
|
-
def
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
# index action should use :list serializer config if :index is not provided
|
51
|
-
action = :list if action == :index && !action_serializer_config.key?(:index)
|
58
|
+
# Get the list of filtering backends to use.
|
59
|
+
def get_filter_backends
|
60
|
+
backends = super
|
61
|
+
return backends if backends
|
52
62
|
|
53
|
-
return
|
63
|
+
# By default, return the standard model filter backend.
|
64
|
+
return [RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter]
|
54
65
|
end
|
55
66
|
|
56
|
-
# Get a list of
|
57
|
-
def
|
58
|
-
|
67
|
+
# Get a list of fields for the current action.
|
68
|
+
def get_fields
|
69
|
+
action_fields = self.class.action_fields || {}
|
59
70
|
action = self.action_name.to_sym
|
60
71
|
|
61
|
-
# index action should use :list
|
62
|
-
action = :list if action == :index && !
|
72
|
+
# index action should use :list fields if :index is not provided
|
73
|
+
action = :list if action == :index && !action_fields.key?(:index)
|
63
74
|
|
64
|
-
return (
|
75
|
+
return (action_fields[action] if action) || self.class.fields || []
|
65
76
|
end
|
66
77
|
|
67
78
|
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
68
79
|
def _get_parameter_values_from_request_body
|
69
80
|
fields = self.get_allowed_parameters || self.get_fields
|
70
|
-
return @
|
81
|
+
return @_get_parameter_values_from_request_body ||= (request.request_parameters.select { |p|
|
71
82
|
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
72
83
|
})
|
73
84
|
end
|
74
85
|
alias :get_create_params :_get_parameter_values_from_request_body
|
75
86
|
alias :get_update_params :_get_parameter_values_from_request_body
|
76
87
|
|
77
|
-
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
78
|
-
def _get_filterset_values_from_params
|
79
|
-
fields = self.filterset_fields || self.get_fields
|
80
|
-
return @_get_field_values_from_params ||= request.query_parameters.select { |p|
|
81
|
-
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
82
|
-
}
|
83
|
-
end
|
84
|
-
alias :get_lookup_params :_get_filterset_values_from_params
|
85
|
-
alias :get_filter_params :_get_filterset_values_from_params
|
86
|
-
|
87
|
-
# Get the recordset, filtered by the filter params.
|
88
|
-
def get_filtered_recordset
|
89
|
-
filter_params = self.get_filter_params
|
90
|
-
unless filter_params.blank?
|
91
|
-
return self.get_recordset.where(**self.get_filter_params.to_hash.symbolize_keys)
|
92
|
-
end
|
93
|
-
return self.get_recordset
|
94
|
-
end
|
95
|
-
|
96
88
|
# Get a record by `id` or return a single record if recordset is filtered down to a single
|
97
89
|
# record.
|
98
90
|
def get_record
|
99
|
-
records = self.
|
91
|
+
records = self.get_filtered_data(self.get_recordset)
|
100
92
|
if params['id'] # direct lookup
|
101
93
|
return records.find(params['id'])
|
102
94
|
elsif records.length == 1
|
@@ -143,33 +135,42 @@ module RESTFramework
|
|
143
135
|
end
|
144
136
|
|
145
137
|
module ListModelMixin
|
146
|
-
# TODO: pagination classes like Django
|
147
138
|
def index
|
148
|
-
@records = self.
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
139
|
+
@records = self.get_filtered_data(self.get_recordset)
|
140
|
+
|
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
|
149
|
+
end
|
150
|
+
|
151
|
+
return api_response(data)
|
153
152
|
end
|
154
153
|
end
|
155
154
|
|
156
155
|
module ShowModelMixin
|
157
156
|
def show
|
158
157
|
@record = self.get_record
|
159
|
-
|
160
|
-
|
161
|
-
).serialize
|
162
|
-
return api_response(@serialized_record)
|
158
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
159
|
+
return api_response(serialized_record)
|
163
160
|
end
|
164
161
|
end
|
165
162
|
|
166
163
|
module CreateModelMixin
|
167
164
|
def create
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
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)
|
173
174
|
end
|
174
175
|
end
|
175
176
|
|
@@ -177,10 +178,8 @@ module RESTFramework
|
|
177
178
|
def update
|
178
179
|
@record = self.get_record
|
179
180
|
@record.update!(self.get_update_params)
|
180
|
-
|
181
|
-
|
182
|
-
).serialize
|
183
|
-
return api_response(@serialized_record)
|
181
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
182
|
+
return api_response(serialized_record)
|
184
183
|
end
|
185
184
|
end
|
186
185
|
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module RESTFramework
|
2
|
+
class BaseFilter
|
3
|
+
def initialize(controller:)
|
4
|
+
@controller = controller
|
5
|
+
end
|
6
|
+
|
7
|
+
def get_filtered_data(data)
|
8
|
+
raise NotImplementedError
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
# A simple filtering backend that supports filtering a recordset based on fields defined on the
|
13
|
+
# controller class.
|
14
|
+
class ModelFilter < BaseFilter
|
15
|
+
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
16
|
+
def _get_filter_params
|
17
|
+
fields = @controller.class.filterset_fields || @controller.send(:get_fields)
|
18
|
+
return @controller.request.query_parameters.select { |p|
|
19
|
+
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
20
|
+
}.to_hash.symbolize_keys
|
21
|
+
end
|
22
|
+
|
23
|
+
def get_filtered_data(data)
|
24
|
+
filter_params = self._get_filter_params
|
25
|
+
unless filter_params.blank?
|
26
|
+
return data.where(**filter_params)
|
27
|
+
end
|
28
|
+
|
29
|
+
return data
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# A filter backend which handles ordering of the recordset.
|
34
|
+
class ModelOrderingFilter < BaseFilter
|
35
|
+
# Convert ordering string to an ordering configuration.
|
36
|
+
def _get_ordering
|
37
|
+
return nil unless @controller.class.ordering_query_param
|
38
|
+
|
39
|
+
order_string = @controller.params[@controller.class.ordering_query_param]
|
40
|
+
unless order_string.blank?
|
41
|
+
return order_string.split(',').map { |field|
|
42
|
+
if field[0] == '-'
|
43
|
+
[field[1..-1].to_sym, :desc]
|
44
|
+
else
|
45
|
+
[field.to_sym, :asc]
|
46
|
+
end
|
47
|
+
}.to_h
|
48
|
+
end
|
49
|
+
|
50
|
+
return nil
|
51
|
+
end
|
52
|
+
|
53
|
+
def get_filtered_data(data)
|
54
|
+
ordering = self._get_ordering
|
55
|
+
if ordering && !ordering.empty?
|
56
|
+
return data.order(_get_ordering)
|
57
|
+
end
|
58
|
+
return data
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
# TODO: implement searching within fields rather than exact match filtering (ModelFilter)
|
63
|
+
# class ModelSearchFilter < BaseFilter
|
64
|
+
# end
|
65
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
module RESTFramework
|
2
|
+
|
3
|
+
# A simple paginator based on page numbers.
|
4
|
+
#
|
5
|
+
# Example: http://example.com/api/users/?page=3&page_size=50
|
6
|
+
class PageNumberPaginator
|
7
|
+
def initialize(data:, controller:, **kwargs)
|
8
|
+
@data = data
|
9
|
+
@controller = controller
|
10
|
+
@count = data.count
|
11
|
+
@page_size = self._page_size
|
12
|
+
|
13
|
+
@total_pages = @count / @page_size
|
14
|
+
@total_pages += 1 if @count % @page_size
|
15
|
+
end
|
16
|
+
|
17
|
+
def _page_size
|
18
|
+
page_size = nil
|
19
|
+
|
20
|
+
# Get from context, if allowed.
|
21
|
+
if @controller.class.page_size_query_param
|
22
|
+
page_size = @controller.params[@controller.class.page_size_query_param].presence
|
23
|
+
if page_size
|
24
|
+
page_size = page_size.to_i
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Otherwise, get from config.
|
29
|
+
if !page_size && @controller.class.page_size
|
30
|
+
page_size = @controller.class.page_size
|
31
|
+
end
|
32
|
+
|
33
|
+
# Fallback to a page size of 15.
|
34
|
+
page_size = 15 unless page_size
|
35
|
+
|
36
|
+
# Ensure we don't exceed the max page size.
|
37
|
+
if @controller.class.max_page_size && page_size > @controller.class.max_page_size
|
38
|
+
page_size = @controller.class.max_page_size
|
39
|
+
end
|
40
|
+
|
41
|
+
# Ensure we return at least 1.
|
42
|
+
return page_size.zero? ? 1 : page_size
|
43
|
+
end
|
44
|
+
|
45
|
+
def _page_query_param
|
46
|
+
return @controller.class.page_query_param&.to_sym
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_page(page_number=nil)
|
50
|
+
# If page number isn't provided, infer from the params or use 1 as a fallback value.
|
51
|
+
if !page_number
|
52
|
+
page_number = @controller&.params&.[](self._page_query_param)
|
53
|
+
if page_number.blank?
|
54
|
+
page_number = 1
|
55
|
+
else
|
56
|
+
page_number = page_number.to_i
|
57
|
+
if page_number.zero?
|
58
|
+
page_number = 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
@page_number = page_number
|
63
|
+
|
64
|
+
# Get the data page and return it so the caller can serialize the data in the proper format.
|
65
|
+
page_index = @page_number - 1
|
66
|
+
return @data.limit(@page_size).offset(page_index * @page_size)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Wrap the serialized page with appripriate metadata.
|
70
|
+
def get_paginated_response(serialized_page)
|
71
|
+
return {
|
72
|
+
count: @count,
|
73
|
+
page: @page_number,
|
74
|
+
total_pages: @total_pages,
|
75
|
+
results: serialized_page,
|
76
|
+
}
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
# TODO: implement this
|
81
|
+
# class CountOffsetPaginator
|
82
|
+
# end
|
83
|
+
|
84
|
+
end
|
@@ -2,9 +2,8 @@ module RESTFramework
|
|
2
2
|
class BaseSerializer
|
3
3
|
attr_reader :errors
|
4
4
|
|
5
|
-
def initialize(object: nil,
|
5
|
+
def initialize(object: nil, controller: nil, **kwargs)
|
6
6
|
@object = object
|
7
|
-
@data = data
|
8
7
|
@controller = controller
|
9
8
|
end
|
10
9
|
end
|
@@ -12,16 +11,15 @@ module RESTFramework
|
|
12
11
|
# This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is a Rails
|
13
12
|
# method which converts objects to Ruby primitives (with the top-level being either an array or a
|
14
13
|
# hash).
|
15
|
-
class
|
14
|
+
class NativeSerializer < BaseSerializer
|
16
15
|
class_attribute :config
|
17
16
|
class_attribute :singular_config
|
18
17
|
class_attribute :plural_config
|
19
18
|
class_attribute :action_config
|
20
19
|
|
21
|
-
def initialize(
|
20
|
+
def initialize(many: nil, **kwargs)
|
22
21
|
super(**kwargs)
|
23
22
|
@many = many
|
24
|
-
@model = model || (@controller ? @controller.send(:get_model) : nil)
|
25
23
|
end
|
26
24
|
|
27
25
|
# Get controller action, if possible.
|
@@ -34,88 +32,94 @@ module RESTFramework
|
|
34
32
|
action = self.get_action
|
35
33
|
|
36
34
|
if action && self.action_config
|
37
|
-
#
|
35
|
+
# Index action should use :list serializer config if :index is not provided.
|
38
36
|
action = :list if action == :index && !self.action_config.key?(:index)
|
39
37
|
|
40
38
|
return self.action_config[action] if self.action_config[action]
|
41
39
|
end
|
42
40
|
|
43
|
-
#
|
41
|
+
# No action_config, so try singular/plural config.
|
44
42
|
return self.plural_config if @many && self.plural_config
|
45
43
|
return self.singular_config if !@many && self.singular_config
|
46
44
|
|
47
|
-
#
|
45
|
+
# Lastly, try returning the default config.
|
48
46
|
return self.config
|
49
47
|
end
|
50
48
|
|
51
|
-
# Get a configuration passable to `as_json` for the
|
49
|
+
# Get a configuration passable to `as_json` for the object.
|
52
50
|
def get_serializer_config
|
53
|
-
#
|
54
|
-
local_config = self.get_local_serializer_config
|
55
|
-
|
56
|
-
|
57
|
-
# return a serializer config if one is defined
|
58
|
-
serializer_config = @controller.send(:get_native_serializer_config)
|
59
|
-
return serializer_config if serializer_config
|
51
|
+
# Return a locally defined serializer config if one is defined.
|
52
|
+
if local_config = self.get_local_serializer_config
|
53
|
+
return local_config
|
54
|
+
end
|
60
55
|
|
61
|
-
#
|
62
|
-
|
63
|
-
|
64
|
-
columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
|
65
|
-
return {only: columns, methods: methods}
|
56
|
+
# Return a serializer config if one is defined.
|
57
|
+
if serializer_config = @controller.send(:get_native_serializer_config)
|
58
|
+
return serializer_config
|
66
59
|
end
|
67
60
|
|
68
61
|
return {}
|
69
62
|
end
|
70
63
|
|
71
|
-
#
|
72
|
-
def
|
73
|
-
|
74
|
-
|
75
|
-
return
|
64
|
+
# Convert the object (record or recordset) to Ruby primitives.
|
65
|
+
def serialize
|
66
|
+
if @object
|
67
|
+
@many = @object.respond_to?(:each) if @many.nil?
|
68
|
+
return @object.as_json(self.get_serializer_config)
|
76
69
|
end
|
70
|
+
return nil
|
71
|
+
end
|
77
72
|
|
78
|
-
|
79
|
-
|
80
|
-
|
73
|
+
# Allow a serializer instance to be used as a hash directly in a nested serializer config.
|
74
|
+
def [](key)
|
75
|
+
unless instance_variable_defined?(:@_nested_config)
|
76
|
+
@_nested_config = self.get_serializer_config
|
81
77
|
end
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
78
|
+
return @_nested_config[key]
|
79
|
+
end
|
80
|
+
def []=(key, value)
|
81
|
+
unless instance_variable_defined?(:@_nested_config)
|
82
|
+
@_nested_config = self.get_serializer_config
|
86
83
|
end
|
84
|
+
return @_nested_config[key] = value
|
85
|
+
end
|
87
86
|
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
end
|
93
|
-
else
|
94
|
-
node.map! do |v|
|
95
|
-
self._resolve_serializer_config(node: v)
|
96
|
-
end
|
87
|
+
# Allow a serializer class to be used as a hash directly in a nested serializer config.
|
88
|
+
def self.[](key)
|
89
|
+
unless instance_variable_defined?(:@_nested_config)
|
90
|
+
@_nested_config = self.new.get_serializer_config
|
97
91
|
end
|
98
|
-
|
99
|
-
return node
|
92
|
+
return @_nested_config[key]
|
100
93
|
end
|
94
|
+
def self.[]=(key, value)
|
95
|
+
unless instance_variable_defined?(:@_nested_config)
|
96
|
+
@_nested_config = self.new.get_serializer_config
|
97
|
+
end
|
98
|
+
return @_nested_config[key] = value
|
99
|
+
end
|
100
|
+
end
|
101
101
|
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
102
|
+
# `NativeModelSerializer` is similar to `NativeSerializer` but with some customizations to work
|
103
|
+
# with `ActiveModel`.
|
104
|
+
class NativeModelSerializer < NativeSerializer
|
105
|
+
def initialize(model: nil, **kwargs)
|
106
|
+
super(**kwargs)
|
107
|
+
@model = model || (@controller ? @controller.send(:get_model) : nil)
|
108
108
|
end
|
109
109
|
|
110
|
-
#
|
111
|
-
def
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
110
|
+
# Get a configuration passable to `as_json` for the object.
|
111
|
+
def get_serializer_config
|
112
|
+
config = super
|
113
|
+
return config unless config.blank?
|
114
|
+
|
115
|
+
# If the config wasn't determined, build a serializer config from model fields.
|
116
|
+
fields = @controller.try(:get_fields) if @controller
|
117
|
+
unless fields.blank?
|
118
|
+
columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
|
119
|
+
return {only: columns, methods: methods}
|
117
120
|
end
|
118
|
-
|
121
|
+
|
122
|
+
return {}
|
119
123
|
end
|
120
124
|
end
|
121
125
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rest_framework
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.16
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Gregory N. Schmit
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-07 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -43,6 +43,8 @@ files:
|
|
43
43
|
- lib/rest_framework/controller_mixins/base.rb
|
44
44
|
- lib/rest_framework/controller_mixins/models.rb
|
45
45
|
- lib/rest_framework/engine.rb
|
46
|
+
- lib/rest_framework/filters.rb
|
47
|
+
- lib/rest_framework/paginators.rb
|
46
48
|
- lib/rest_framework/routers.rb
|
47
49
|
- lib/rest_framework/serializers.rb
|
48
50
|
- lib/rest_framework/version.rb
|