rest_framework 0.0.12 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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.
|