rest_framework 0.0.12 → 0.1.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 +4 -4
- data/README.md +3 -15
- data/app/views/layouts/rest_framework.html.erb +2 -2
- data/lib/rest_framework.rb +3 -0
- data/lib/rest_framework/VERSION_STAMP +1 -1
- data/lib/rest_framework/controller_mixins/base.rb +74 -9
- data/lib/rest_framework/controller_mixins/models.rb +86 -80
- data/lib/rest_framework/filters.rb +65 -0
- data/lib/rest_framework/paginators.rb +84 -0
- data/lib/rest_framework/routers.rb +84 -41
- data/lib/rest_framework/serializers.rb +28 -20
- metadata +7 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46db4e7aa05600caaa4c1f9f645f417b4d3cd9e5bcde7da0b54d3c5fa84e872e
|
4
|
+
data.tar.gz: 95c9d018b687795f80c38b43289f19ffd44eb1b170d883c164b590ab6b1c20d0
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 2c376d191ffa5ae9de932dceb8411362a00be789ff9aa3733f038608da890fc5437d981576e3b17d402b857f7966cb15e437d7cbe2c8447191088a9a2249134f
|
7
|
+
data.tar.gz: 72e31acb2e66c6d8d2af2dd7375e7732a93b5e66b1c1fedf4c1d781421422c582958d32c6022305cd9cc2ca9dda51e2c2f6c540b1737e11eb3506501ef91b618
|
data/README.md
CHANGED
@@ -116,18 +116,6 @@ To run the full test suite:
|
|
116
116
|
$ rake test
|
117
117
|
```
|
118
118
|
|
119
|
-
To
|
120
|
-
|
121
|
-
|
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`. You can also load the
|
121
|
+
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
|
-
|
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 %>
|
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.1.0
|
@@ -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,40 @@ 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
|
+
paginator_class: nil,
|
40
|
+
page_size: nil,
|
41
|
+
page_query_param: 'page',
|
42
|
+
page_size_query_param: 'page_size',
|
43
|
+
max_page_size: nil,
|
44
|
+
serializer_class: nil,
|
45
|
+
singleton_controller: nil,
|
46
|
+
skip_actions: nil,
|
47
|
+
}.each do |a, default|
|
48
|
+
unless base.respond_to?(a)
|
49
|
+
base.class_attribute(a)
|
50
|
+
|
51
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the
|
52
|
+
# default parameter on `class_attribute`.
|
53
|
+
base.send(:"#{a}=", default)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Alias `extra_actions` to `extra_collection_actions`.
|
58
|
+
unless base.respond_to?(:extra_collection_actions)
|
59
|
+
base.alias_method(:extra_collection_actions, :extra_actions)
|
60
|
+
base.alias_method(:extra_collection_actions=, :extra_actions=)
|
61
|
+
end
|
62
|
+
|
63
|
+
# skip csrf since this is an API
|
64
|
+
base.skip_before_action(:verify_authenticity_token) rescue nil
|
65
|
+
|
66
|
+
# handle some common exceptions
|
37
67
|
base.rescue_from(ActiveRecord::RecordNotFound, with: :record_not_found)
|
38
68
|
base.rescue_from(ActiveRecord::RecordInvalid, with: :record_invalid)
|
39
69
|
base.rescue_from(ActiveRecord::RecordNotSaved, with: :record_not_saved)
|
@@ -43,6 +73,40 @@ module RESTFramework
|
|
43
73
|
|
44
74
|
protected
|
45
75
|
|
76
|
+
# Helper to get filtering backends with a sane default.
|
77
|
+
def get_filter_backends
|
78
|
+
return self.class.filter_backends || []
|
79
|
+
end
|
80
|
+
|
81
|
+
# Filter the recordset over all configured filter backends.
|
82
|
+
def get_filtered_data(data)
|
83
|
+
self.get_filter_backends.each do |filter_class|
|
84
|
+
filter = filter_class.new(controller: self)
|
85
|
+
data = filter.get_filtered_data(data)
|
86
|
+
end
|
87
|
+
|
88
|
+
return data
|
89
|
+
end
|
90
|
+
|
91
|
+
# Helper to get the configured serializer class.
|
92
|
+
def get_serializer_class
|
93
|
+
return self.class.serializer_class
|
94
|
+
end
|
95
|
+
|
96
|
+
# Get a native serializer config for the current action.
|
97
|
+
def get_native_serializer_config
|
98
|
+
action_serializer_config = self.class.native_serializer_action_config || {}
|
99
|
+
action = self.action_name.to_sym
|
100
|
+
|
101
|
+
# Handle case where :index action is not defined.
|
102
|
+
if action == :index && !action_serializer_config.key?(:index)
|
103
|
+
# Default is :show if `singleton_controller`, otherwise :list.
|
104
|
+
action = self.class.singleton_controller ? :show : :list
|
105
|
+
end
|
106
|
+
|
107
|
+
return (action_serializer_config[action] if action) || self.class.native_serializer_config
|
108
|
+
end
|
109
|
+
|
46
110
|
def record_invalid(e)
|
47
111
|
return api_response(
|
48
112
|
{message: "Record invalid.", exception: e, errors: e.record.errors}, status: 400
|
@@ -61,6 +125,7 @@ module RESTFramework
|
|
61
125
|
return api_response({message: "Record not destroyed.", exception: e}, status: 406)
|
62
126
|
end
|
63
127
|
|
128
|
+
# Helper for showing routes under a controller action, used for the browsable API.
|
64
129
|
def _get_routes
|
65
130
|
begin
|
66
131
|
formatter = ActionDispatch::Routing::ConsoleFormatter::Sheet
|
@@ -69,14 +134,14 @@ module RESTFramework
|
|
69
134
|
end
|
70
135
|
return ActionDispatch::Routing::RoutesInspector.new(Rails.application.routes.routes).format(
|
71
136
|
formatter.new
|
72
|
-
).lines
|
137
|
+
).lines.drop(1).map { |r| r.split.last(3) }.map { |r|
|
73
138
|
{verb: r[0], path: r[1], action: r[2]}
|
74
139
|
}.select { |r| r[:path].start_with?(request.path) }
|
75
140
|
end
|
76
141
|
|
77
142
|
# Helper alias for `respond_to`/`render`. `payload` should be already serialized to Ruby
|
78
143
|
# primitives.
|
79
|
-
def api_response(payload
|
144
|
+
def api_response(payload, html_kwargs: nil, json_kwargs: nil, xml_kwargs: nil, **kwargs)
|
80
145
|
html_kwargs ||= {}
|
81
146
|
json_kwargs ||= {}
|
82
147
|
xml_kwargs ||= {}
|
@@ -119,8 +184,8 @@ module RESTFramework
|
|
119
184
|
rescue ActionView::MissingTemplate # fallback to rest_framework layout/view
|
120
185
|
kwargs[:layout] = "rest_framework"
|
121
186
|
kwargs[:template] = "rest_framework/default"
|
187
|
+
render(**kwargs)
|
122
188
|
end
|
123
|
-
render(**kwargs)
|
124
189
|
}
|
125
190
|
end
|
126
191
|
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,94 @@ 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 the default native serializer.
|
27
|
+
native_serializer_config: nil,
|
28
|
+
native_serializer_action_config: nil,
|
29
|
+
|
30
|
+
# Attributes for default model filtering (and ordering).
|
31
|
+
filterset_fields: nil,
|
32
|
+
ordering_fields: nil,
|
33
|
+
ordering_query_param: 'ordering',
|
34
|
+
|
35
|
+
# Other misc attributes.
|
36
|
+
disable_creation_from_recordset: nil, # Option to disable `recordset.create` behavior.
|
37
|
+
}.each do |a, default|
|
38
|
+
unless base.respond_to?(a)
|
39
|
+
base.class_attribute(a)
|
40
|
+
|
41
|
+
# Set default manually so we can still support Rails 4. Maybe later we can use the
|
42
|
+
# default parameter on `class_attribute`.
|
43
|
+
base.send(:"#{a}=", default)
|
44
|
+
end
|
45
|
+
end
|
25
46
|
end
|
26
47
|
end
|
27
48
|
|
28
49
|
protected
|
29
50
|
|
51
|
+
# Helper to get the configured serializer class, or `NativeModelSerializer` as a default.
|
30
52
|
def get_serializer_class
|
31
|
-
return self.class.serializer_class || NativeModelSerializer
|
53
|
+
return self.class.serializer_class || RESTFramework::NativeModelSerializer
|
32
54
|
end
|
33
55
|
|
34
|
-
# Get a list of
|
35
|
-
def
|
36
|
-
|
56
|
+
# Get a list of parameters allowed for the current action.
|
57
|
+
def get_allowed_parameters
|
58
|
+
allowed_action_parameters = self.class.allowed_action_parameters || {}
|
37
59
|
action = self.action_name.to_sym
|
38
60
|
|
39
|
-
# index action should use :list
|
40
|
-
action = :list if action == :index && !
|
61
|
+
# index action should use :list allowed parameters if :index is not provided
|
62
|
+
action = :list if action == :index && !allowed_action_parameters.key?(:index)
|
41
63
|
|
42
|
-
return (
|
64
|
+
return (allowed_action_parameters[action] if action) || self.class.allowed_parameters
|
43
65
|
end
|
44
66
|
|
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)
|
52
|
-
|
53
|
-
return (action_serializer_config[action] if action) || self.class.native_serializer_config
|
67
|
+
# Get the list of filtering backends to use.
|
68
|
+
def get_filter_backends
|
69
|
+
return self.class.filter_backends || [
|
70
|
+
RESTFramework::ModelFilter, RESTFramework::ModelOrderingFilter
|
71
|
+
]
|
54
72
|
end
|
55
73
|
|
56
|
-
# Get a list of
|
57
|
-
def
|
58
|
-
|
74
|
+
# Get a list of fields for the current action.
|
75
|
+
def get_fields
|
76
|
+
action_fields = self.class.action_fields || {}
|
59
77
|
action = self.action_name.to_sym
|
60
78
|
|
61
|
-
# index action should use :list
|
62
|
-
action = :list if action == :index && !
|
79
|
+
# index action should use :list fields if :index is not provided
|
80
|
+
action = :list if action == :index && !action_fields.key?(:index)
|
63
81
|
|
64
|
-
return (
|
82
|
+
return (action_fields[action] if action) || self.class.fields || []
|
65
83
|
end
|
66
84
|
|
67
85
|
# Filter the request body for keys in current action's allowed_parameters/fields config.
|
68
|
-
def
|
86
|
+
def get_body_params
|
69
87
|
fields = self.get_allowed_parameters || self.get_fields
|
70
|
-
return @
|
88
|
+
return @get_body_params ||= (request.request_parameters.select { |p|
|
71
89
|
fields.include?(p.to_sym) || fields.include?(p.to_s)
|
72
90
|
})
|
73
91
|
end
|
74
|
-
alias :get_create_params :
|
75
|
-
alias :get_update_params :
|
76
|
-
|
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_filterset_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
|
92
|
+
alias :get_create_params :get_body_params
|
93
|
+
alias :get_update_params :get_body_params
|
95
94
|
|
96
95
|
# Get a record by `id` or return a single record if recordset is filtered down to a single
|
97
96
|
# record.
|
98
97
|
def get_record
|
99
|
-
records = self.
|
98
|
+
records = self.get_filtered_data(self.get_recordset)
|
100
99
|
if params['id'] # direct lookup
|
101
100
|
return records.find(params['id'])
|
102
101
|
elsif records.length == 1
|
@@ -143,33 +142,42 @@ module RESTFramework
|
|
143
142
|
end
|
144
143
|
|
145
144
|
module ListModelMixin
|
146
|
-
# TODO: pagination classes like Django
|
147
145
|
def index
|
148
|
-
@records = self.
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
146
|
+
@records = self.get_filtered_data(self.get_recordset)
|
147
|
+
|
148
|
+
# Handle pagination, if enabled.
|
149
|
+
if self.class.paginator_class
|
150
|
+
paginator = self.class.paginator_class.new(data: @records, controller: self)
|
151
|
+
page = paginator.get_page
|
152
|
+
serialized_page = self.get_serializer_class.new(object: page, controller: self).serialize
|
153
|
+
data = paginator.get_paginated_response(serialized_page)
|
154
|
+
else
|
155
|
+
data = self.get_serializer_class.new(object: @records, controller: self).serialize
|
156
|
+
end
|
157
|
+
|
158
|
+
return api_response(data)
|
153
159
|
end
|
154
160
|
end
|
155
161
|
|
156
162
|
module ShowModelMixin
|
157
163
|
def show
|
158
164
|
@record = self.get_record
|
159
|
-
|
160
|
-
|
161
|
-
).serialize
|
162
|
-
return api_response(@serialized_record)
|
165
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
166
|
+
return api_response(serialized_record)
|
163
167
|
end
|
164
168
|
end
|
165
169
|
|
166
170
|
module CreateModelMixin
|
167
171
|
def create
|
168
|
-
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
172
|
+
if self.get_recordset.respond_to?(:create!) && !self.disable_creation_from_recordset
|
173
|
+
# Create with any properties inherited from the recordset (like associations).
|
174
|
+
@record = self.get_recordset.create!(self.get_create_params)
|
175
|
+
else
|
176
|
+
# Otherwise, perform a "bare" create.
|
177
|
+
@record = self.get_model.create!(self.get_create_params)
|
178
|
+
end
|
179
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
180
|
+
return api_response(serialized_record)
|
173
181
|
end
|
174
182
|
end
|
175
183
|
|
@@ -177,10 +185,8 @@ module RESTFramework
|
|
177
185
|
def update
|
178
186
|
@record = self.get_record
|
179
187
|
@record.update!(self.get_update_params)
|
180
|
-
|
181
|
-
|
182
|
-
).serialize
|
183
|
-
return api_response(@serialized_record)
|
188
|
+
serialized_record = self.get_serializer_class.new(object: @record, controller: self).serialize
|
189
|
+
return api_response(serialized_record)
|
184
190
|
end
|
185
191
|
end
|
186
192
|
|
@@ -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 != 0)
|
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,6 +2,41 @@ require 'action_dispatch/routing/mapper'
|
|
2
2
|
|
3
3
|
module ActionDispatch::Routing
|
4
4
|
class Mapper
|
5
|
+
# Helper to take extra_actions hash and convert to a consistent format.
|
6
|
+
protected def _parse_extra_actions(extra_actions)
|
7
|
+
return (extra_actions || {}).map do |k,v|
|
8
|
+
kwargs = {}
|
9
|
+
path = k
|
10
|
+
|
11
|
+
# Convert structure to path/methods/kwargs.
|
12
|
+
if v.is_a?(Hash) # allow kwargs
|
13
|
+
v = v.symbolize_keys
|
14
|
+
|
15
|
+
# Ensure methods is an array.
|
16
|
+
if v[:methods].is_a?(String) || v[:methods].is_a?(Symbol)
|
17
|
+
methods = [v.delete(:methods)]
|
18
|
+
else
|
19
|
+
methods = v.delete(:methods)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Override path if it's provided.
|
23
|
+
if v.key?(:path)
|
24
|
+
path = v.delete(:path)
|
25
|
+
end
|
26
|
+
|
27
|
+
# Pass any further kwargs to the underlying Rails interface.
|
28
|
+
kwargs = kwargs.merge(v)
|
29
|
+
elsif v.is_a?(Symbol) || v.is_a?(String)
|
30
|
+
methods = [v]
|
31
|
+
else
|
32
|
+
methods = v
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return a hash with keys: :path, :methods, :kwargs.
|
36
|
+
{path: path, methods: methods, kwargs: kwargs}
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
5
40
|
# Private interface to get the controller class from the name and current scope.
|
6
41
|
protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
|
7
42
|
# get class name
|
@@ -40,16 +75,16 @@ module ActionDispatch::Routing
|
|
40
75
|
# @param default_singular [Boolean] the default plurality of the resource if the plurality is
|
41
76
|
# not otherwise defined by the controller
|
42
77
|
# @param name [Symbol] the resource name, from which path and controller are deduced by default
|
43
|
-
# @param skip_undefined [Boolean] whether we should skip routing undefined actions
|
78
|
+
# @param skip_undefined [Boolean] whether we should skip routing undefined resourceful actions
|
44
79
|
protected def _rest_resources(default_singular, name, skip_undefined: true, **kwargs, &block)
|
45
|
-
controller = kwargs
|
80
|
+
controller = kwargs.delete(:controller) || name
|
46
81
|
if controller.is_a?(Class)
|
47
82
|
controller_class = controller
|
48
83
|
else
|
49
84
|
controller_class = _get_controller_class(controller, pluralize: !default_singular)
|
50
85
|
end
|
51
86
|
|
52
|
-
#
|
87
|
+
# Set controller if it's not explicitly set.
|
53
88
|
kwargs[:controller] = name unless kwargs[:controller]
|
54
89
|
|
55
90
|
# determine plural/singular resource
|
@@ -70,25 +105,20 @@ module ActionDispatch::Routing
|
|
70
105
|
public_send(resource_method, name, except: skip, **kwargs) do
|
71
106
|
if controller_class.respond_to?(:extra_member_actions)
|
72
107
|
member do
|
73
|
-
actions = controller_class.extra_member_actions
|
74
|
-
actions
|
75
|
-
|
76
|
-
|
77
|
-
methods.each do |m|
|
78
|
-
public_send(m, action)
|
108
|
+
actions = self._parse_extra_actions(controller_class.extra_member_actions)
|
109
|
+
actions.each do |action_config|
|
110
|
+
action_config[:methods].each do |m|
|
111
|
+
public_send(m, action_config[:path], **action_config[:kwargs])
|
79
112
|
end
|
80
113
|
end
|
81
114
|
end
|
82
115
|
end
|
83
116
|
|
84
117
|
collection do
|
85
|
-
actions = controller_class.extra_actions
|
86
|
-
actions
|
87
|
-
|
88
|
-
|
89
|
-
methods = [methods] if methods.is_a?(Symbol) || methods.is_a?(String)
|
90
|
-
methods.each do |m|
|
91
|
-
public_send(m, action)
|
118
|
+
actions = self._parse_extra_actions(controller_class.extra_actions)
|
119
|
+
actions.each do |action_config|
|
120
|
+
action_config[:methods].each do |m|
|
121
|
+
public_send(m, action_config[:path], **action_config[:kwargs])
|
92
122
|
end
|
93
123
|
end
|
94
124
|
end
|
@@ -112,42 +142,55 @@ module ActionDispatch::Routing
|
|
112
142
|
end
|
113
143
|
|
114
144
|
# Route a controller without the default resourceful paths.
|
115
|
-
def rest_route(
|
116
|
-
controller = kwargs.delete(:controller) ||
|
117
|
-
|
145
|
+
def rest_route(name=nil, **kwargs, &block)
|
146
|
+
controller = kwargs.delete(:controller) || name
|
147
|
+
if controller.is_a?(Class)
|
148
|
+
controller_class = controller
|
149
|
+
else
|
150
|
+
controller_class = self._get_controller_class(controller, pluralize: false)
|
151
|
+
end
|
118
152
|
|
119
|
-
#
|
120
|
-
|
121
|
-
|
122
|
-
actions
|
123
|
-
actions =
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
153
|
+
# Set controller if it's not explicitly set.
|
154
|
+
kwargs[:controller] = name unless kwargs[:controller]
|
155
|
+
|
156
|
+
# Route actions using the resourceful router, but skip all builtin actions.
|
157
|
+
actions = self._parse_extra_actions(controller_class.extra_actions)
|
158
|
+
public_send(:resource, name, only: [], **kwargs) do
|
159
|
+
actions.each do |action_config|
|
160
|
+
action_config[:methods].each do |m|
|
161
|
+
public_send(m, action_config[:path], **action_config[:kwargs])
|
162
|
+
end
|
163
|
+
yield if block_given?
|
129
164
|
end
|
130
|
-
yield if block_given?
|
131
165
|
end
|
132
166
|
end
|
133
167
|
|
134
168
|
# Route a controller's `#root` to '/' in the current scope/namespace, along with other actions.
|
135
169
|
# @param label [Symbol] the snake_case name of the controller
|
136
|
-
def rest_root(
|
137
|
-
#
|
170
|
+
def rest_root(name=nil, **kwargs, &block)
|
171
|
+
# By default, use RootController#root.
|
138
172
|
root_action = kwargs.delete(:action) || :root
|
139
|
-
controller = kwargs.delete(:controller) ||
|
140
|
-
path = path.to_s
|
173
|
+
controller = kwargs.delete(:controller) || name || :root
|
141
174
|
|
142
|
-
#
|
143
|
-
get
|
175
|
+
# Route the root.
|
176
|
+
get name.to_s, controller: controller, action: root_action
|
144
177
|
|
145
|
-
#
|
178
|
+
# Route any additional actions.
|
146
179
|
controller_class = self._get_controller_class(controller, pluralize: false)
|
147
|
-
(controller_class.extra_actions
|
148
|
-
|
149
|
-
|
150
|
-
|
180
|
+
actions = self._parse_extra_actions(controller_class.extra_actions)
|
181
|
+
actions.each do |action_config|
|
182
|
+
# Add :action unless kwargs defines it.
|
183
|
+
unless action_config[:kwargs].key?(:action)
|
184
|
+
action_config[:kwargs][:action] = action_config[:path]
|
185
|
+
end
|
186
|
+
|
187
|
+
action_config[:methods].each do |m|
|
188
|
+
public_send(
|
189
|
+
m,
|
190
|
+
File.join(name.to_s, action_config[:path].to_s),
|
191
|
+
controller: controller,
|
192
|
+
**action_config[:kwargs],
|
193
|
+
)
|
151
194
|
end
|
152
195
|
yield if block_given?
|
153
196
|
end
|
@@ -2,25 +2,30 @@ 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
|
11
10
|
|
12
|
-
# This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is
|
13
|
-
# method which converts objects to Ruby primitives (with the top-level being either
|
14
|
-
# hash).
|
11
|
+
# This serializer uses `.as_json` to serialize objects. Despite the name, `.as_json` is an
|
12
|
+
# `ActiveModel` method which converts objects to Ruby primitives (with the top-level being either
|
13
|
+
# an array or a hash).
|
15
14
|
class NativeModelSerializer < 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, model: nil, **kwargs)
|
22
21
|
super(**kwargs)
|
23
|
-
|
22
|
+
|
23
|
+
if many.nil?
|
24
|
+
@many = @object.respond_to?(:count) ? @object.count : nil
|
25
|
+
else
|
26
|
+
@many = many
|
27
|
+
end
|
28
|
+
|
24
29
|
@model = model || (@controller ? @controller.send(:get_model) : nil)
|
25
30
|
end
|
26
31
|
|
@@ -34,32 +39,34 @@ module RESTFramework
|
|
34
39
|
action = self.get_action
|
35
40
|
|
36
41
|
if action && self.action_config
|
37
|
-
#
|
42
|
+
# Index action should use :list serializer config if :index is not provided.
|
38
43
|
action = :list if action == :index && !self.action_config.key?(:index)
|
39
44
|
|
40
45
|
return self.action_config[action] if self.action_config[action]
|
41
46
|
end
|
42
47
|
|
43
|
-
#
|
48
|
+
# No action_config, so try singular/plural config.
|
44
49
|
return self.plural_config if @many && self.plural_config
|
45
50
|
return self.singular_config if !@many && self.singular_config
|
46
51
|
|
47
|
-
#
|
52
|
+
# Lastly, try returning the default config.
|
48
53
|
return self.config
|
49
54
|
end
|
50
55
|
|
51
|
-
# Get a configuration passable to `as_json` for the
|
56
|
+
# Get a configuration passable to `as_json` for the object.
|
52
57
|
def get_serializer_config
|
53
|
-
#
|
54
|
-
local_config = self.get_local_serializer_config
|
55
|
-
|
58
|
+
# Return a locally defined serializer config if one is defined.
|
59
|
+
if local_config = self.get_local_serializer_config
|
60
|
+
return local_config
|
61
|
+
end
|
56
62
|
|
57
|
-
#
|
58
|
-
serializer_config = @controller.send(:get_native_serializer_config)
|
59
|
-
|
63
|
+
# Return a serializer config if one is defined.
|
64
|
+
if serializer_config = @controller.send(:get_native_serializer_config)
|
65
|
+
return serializer_config
|
66
|
+
end
|
60
67
|
|
61
|
-
#
|
62
|
-
fields = @controller.
|
68
|
+
# If the config wasn't determined, build a serializer config from model fields.
|
69
|
+
fields = @controller.try(:get_fields) if @controller
|
63
70
|
unless fields.blank?
|
64
71
|
columns, methods = fields.partition { |f| f.to_s.in?(@model.column_names) }
|
65
72
|
return {only: columns, methods: methods}
|
@@ -68,7 +75,7 @@ module RESTFramework
|
|
68
75
|
return {}
|
69
76
|
end
|
70
77
|
|
71
|
-
# Convert the object(
|
78
|
+
# Convert the object (record or recordset) to Ruby primitives.
|
72
79
|
def serialize
|
73
80
|
if @object
|
74
81
|
@many = @object.respond_to?(:each) if @many.nil?
|
@@ -105,4 +112,5 @@ module RESTFramework
|
|
105
112
|
return @_nested_config[key] = value
|
106
113
|
end
|
107
114
|
end
|
115
|
+
|
108
116
|
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.1.0
|
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-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|
@@ -43,14 +43,16 @@ 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
|
49
|
-
homepage: https://
|
51
|
+
homepage: https://rails-rest-framework.com
|
50
52
|
licenses:
|
51
53
|
- MIT
|
52
54
|
metadata:
|
53
|
-
homepage_uri: https://
|
55
|
+
homepage_uri: https://rails-rest-framework.com
|
54
56
|
source_code_uri: https://github.com/gregschmit/rails-rest-framework
|
55
57
|
post_install_message:
|
56
58
|
rdoc_options: []
|
@@ -68,7 +70,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
68
70
|
- !ruby/object:Gem::Version
|
69
71
|
version: '0'
|
70
72
|
requirements: []
|
71
|
-
rubygems_version: 3.0.
|
73
|
+
rubygems_version: 3.0.9
|
72
74
|
signing_key:
|
73
75
|
specification_version: 4
|
74
76
|
summary: A framework for DRY RESTful APIs in Ruby on Rails.
|