rest_framework 0.5.2 → 0.5.5
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 +4 -4
- data/VERSION +1 -1
- data/app/views/rest_framework/_route.html.erb +8 -6
- data/app/views/rest_framework/_routes.html.erb +1 -1
- data/lib/rest_framework/controller_mixins/base.rb +0 -2
- data/lib/rest_framework/controller_mixins/models.rb +1 -2
- data/lib/rest_framework/filters.rb +3 -3
- data/lib/rest_framework/routers.rb +3 -3
- data/lib/rest_framework/serializers.rb +83 -35
- data/lib/rest_framework/utils.rb +56 -16
- data/lib/rest_framework/version.rb +9 -3
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 237449a323a14356f20e47cd2cbab3591aa749fc7e6f43bc06587995ff1041dc
|
4
|
+
data.tar.gz: 98ff07d840fca2fd09ab608a08a2814c5fe17c9ded1a7476c176941224c75e53
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5219df61d409aeda527a61508694cf5b46dba856f03db4d7a2432d8d208a89eb5ccaccee2d7da88472d5b2735a77b235127d903497b9bb3fb19b740f2eb5df05
|
7
|
+
data.tar.gz: afbc3a40c39d312a0ebaaade7ce8c1d4d00b99fbd3da4168b0e9706d63d2fb59aa6d0f1d56cc4832a6eca10b783f43c20a2f9cf8d1015ef817293ba5852a7a62
|
data/README.md
CHANGED
@@ -120,9 +120,9 @@ using RVM. Then run `bundle install` to install the appropriate gems.
|
|
120
120
|
To run the test suite:
|
121
121
|
|
122
122
|
```shell
|
123
|
-
$
|
123
|
+
$ rails test
|
124
124
|
```
|
125
125
|
|
126
|
-
|
127
|
-
run `
|
128
|
-
|
126
|
+
The top-level `bin/rails` proxies all Rails commands to the test project, so you can operate it via
|
127
|
+
the usual commands. Ensure you run `rails db:setup` before running `rails server` or
|
128
|
+
`rails console`.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.5.
|
1
|
+
0.5.5
|
@@ -1,9 +1,11 @@
|
|
1
1
|
<tr>
|
2
|
-
<td
|
2
|
+
<td>
|
3
|
+
<% if route[:route].name && link_args = route[:show_link_args] %>
|
4
|
+
<%= link_to route[:relative_path], self.send("#{route[:route].name}_path", *link_args) %>
|
5
|
+
<% else %>
|
6
|
+
<%= route[:relative_path] %>
|
7
|
+
<% end %>
|
8
|
+
</td>
|
3
9
|
<td><%= route[:verb] %></td>
|
4
|
-
|
5
|
-
<td><%= route[:controller] %>#<%= route[:action] %></td>
|
6
|
-
<% else %>
|
7
|
-
<td><%= route[:route_app] %></td>
|
8
|
-
<% end %>
|
10
|
+
<td><%= route[:controller] %>#<%= route[:action] %></td>
|
9
11
|
</tr>
|
@@ -31,6 +31,7 @@ module RESTFramework::BaseModelControllerMixin
|
|
31
31
|
native_serializer_config: nil,
|
32
32
|
native_serializer_singular_config: nil,
|
33
33
|
native_serializer_plural_config: nil,
|
34
|
+
native_serializer_only_query_param: "only",
|
34
35
|
native_serializer_except_query_param: "except",
|
35
36
|
|
36
37
|
# Attributes for default model filtering (and ordering).
|
@@ -57,8 +58,6 @@ module RESTFramework::BaseModelControllerMixin
|
|
57
58
|
end
|
58
59
|
end
|
59
60
|
|
60
|
-
protected
|
61
|
-
|
62
61
|
def _get_specific_action_config(action_config_key, generic_config_key)
|
63
62
|
action_config = self.class.send(action_config_key) || {}
|
64
63
|
action = self.action_name&.to_sym
|
@@ -13,7 +13,7 @@ end
|
|
13
13
|
class RESTFramework::ModelFilter < RESTFramework::BaseFilter
|
14
14
|
# Filter params for keys allowed by the current action's filterset_fields/fields config.
|
15
15
|
def _get_filter_params
|
16
|
-
fields = @controller.
|
16
|
+
fields = @controller.get_filterset_fields
|
17
17
|
return @controller.request.query_parameters.select { |p, _|
|
18
18
|
fields.include?(p)
|
19
19
|
}.to_h.symbolize_keys # convert from HashWithIndifferentAccess to Hash w/keys
|
@@ -36,7 +36,7 @@ class RESTFramework::ModelOrderingFilter < RESTFramework::BaseFilter
|
|
36
36
|
def _get_ordering
|
37
37
|
return nil if @controller.class.ordering_query_param.blank?
|
38
38
|
|
39
|
-
ordering_fields = @controller.
|
39
|
+
ordering_fields = @controller.get_ordering_fields
|
40
40
|
order_string = @controller.params[@controller.class.ordering_query_param]
|
41
41
|
|
42
42
|
unless order_string.blank?
|
@@ -76,7 +76,7 @@ end
|
|
76
76
|
class RESTFramework::ModelSearchFilter < RESTFramework::BaseFilter
|
77
77
|
# Filter data according to the request query parameters.
|
78
78
|
def get_filtered_data(data)
|
79
|
-
fields = @controller.
|
79
|
+
fields = @controller.get_search_fields
|
80
80
|
search = @controller.request.query_parameters[@controller.class.search_query_param]
|
81
81
|
|
82
82
|
# Ensure we use array conditions to prevent SQL injection.
|
@@ -4,7 +4,7 @@ require_relative "utils"
|
|
4
4
|
module ActionDispatch::Routing
|
5
5
|
class Mapper
|
6
6
|
# Internal interface to get the controller class from the name and current scope.
|
7
|
-
|
7
|
+
def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
|
8
8
|
# Get class name.
|
9
9
|
name = name.to_s.camelize # camelize to leave plural names plural
|
10
10
|
name = name.pluralize if pluralize
|
@@ -38,7 +38,7 @@ module ActionDispatch::Routing
|
|
38
38
|
end
|
39
39
|
|
40
40
|
# Interal interface for routing extra actions.
|
41
|
-
|
41
|
+
def _route_extra_actions(actions, &block)
|
42
42
|
actions.each do |action_config|
|
43
43
|
action_config[:methods].each do |m|
|
44
44
|
public_send(m, action_config[:path], **action_config[:kwargs])
|
@@ -52,7 +52,7 @@ module ActionDispatch::Routing
|
|
52
52
|
# not otherwise defined by the controller
|
53
53
|
# @param name [Symbol] the resource name, from which path and controller are deduced by default
|
54
54
|
# @param skip_undefined [Boolean] whether we should skip routing undefined resourceful actions
|
55
|
-
|
55
|
+
def _rest_resources(default_singular, name, skip_undefined: true, **kwargs, &block)
|
56
56
|
controller = kwargs.delete(:controller) || name
|
57
57
|
if controller.is_a?(Class)
|
58
58
|
controller_class = controller
|
@@ -13,10 +13,12 @@ class RESTFramework::BaseSerializer
|
|
13
13
|
raise NotImplementedError
|
14
14
|
end
|
15
15
|
|
16
|
+
# :nocov:
|
16
17
|
# Synonym for `serializable_hash` or compatibility with ActiveModelSerializers.
|
17
18
|
def serializable_hash(**kwargs)
|
18
19
|
return self.serialize(**kwargs)
|
19
20
|
end
|
21
|
+
# :nocov:
|
20
22
|
end
|
21
23
|
|
22
24
|
# This serializer uses `.serializable_hash` to convert objects to Ruby primitives (with the
|
@@ -43,7 +45,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
43
45
|
@model ||= @object[0].class if
|
44
46
|
@many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
|
45
47
|
|
46
|
-
@model ||= @controller.
|
48
|
+
@model ||= @controller.get_model if @controller
|
47
49
|
end
|
48
50
|
|
49
51
|
# Get controller action, if possible.
|
@@ -84,57 +86,103 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
84
86
|
end
|
85
87
|
|
86
88
|
# Helper to filter (mutate) a single subconfig for specific keys.
|
87
|
-
def self.
|
88
|
-
return
|
89
|
-
|
90
|
-
if
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
89
|
+
def self.filter_subcfg(subcfg, except: nil, only: nil, additive: false)
|
90
|
+
return subcfg unless except || only
|
91
|
+
return subcfg unless subcfg || additive
|
92
|
+
raise "Cannot pass `only` and `additive` to filter_subcfg." if only && additive
|
93
|
+
|
94
|
+
if subcfg.is_a?(Array)
|
95
|
+
subcfg = subcfg.map(&:to_sym)
|
96
|
+
if except
|
97
|
+
if additive
|
98
|
+
# Only add fields which are not already included.
|
99
|
+
subcfg += except - subcfg
|
100
|
+
else
|
101
|
+
subcfg -= except
|
102
|
+
end
|
103
|
+
elsif only
|
104
|
+
# Ignore `additive` in an `only` context, since it could causing data leaking.
|
105
|
+
unless additive
|
106
|
+
subcfg.select! { |c| c.in?(only) }
|
107
|
+
end
|
97
108
|
end
|
98
|
-
elsif
|
109
|
+
elsif subcfg.is_a?(Hash)
|
99
110
|
# Additive doesn't make sense in a hash context since we wouldn't know the values.
|
100
111
|
unless additive
|
101
|
-
|
102
|
-
|
112
|
+
if except
|
113
|
+
subcfg.symbolize_keys!
|
114
|
+
subcfg.reject! { |k, _v| k.in?(except) }
|
115
|
+
elsif only
|
116
|
+
subcfg.symbolize_keys!
|
117
|
+
subcfg.select! { |k, _v| k.in?(only) }
|
118
|
+
end
|
103
119
|
end
|
104
|
-
elsif !
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
120
|
+
elsif !subcfg
|
121
|
+
if additive && except
|
122
|
+
subcfg = except
|
123
|
+
end
|
124
|
+
else # Subcfg is a single element (assume string/symbol).
|
125
|
+
subcfg = subcfg.to_sym
|
126
|
+
|
127
|
+
if except
|
128
|
+
if subcfg.in?(except)
|
129
|
+
subcfg = [] unless additive
|
130
|
+
elsif additive
|
131
|
+
subcfg = [subcfg, *except]
|
132
|
+
end
|
133
|
+
elsif only && !additive && !subcfg.in?(only) # Protect only/additive data-leaking.
|
134
|
+
subcfg = []
|
111
135
|
end
|
112
136
|
end
|
113
137
|
|
114
|
-
return
|
138
|
+
return subcfg
|
115
139
|
end
|
116
140
|
|
117
141
|
# Helper to filter out configuration properties based on the :except query parameter.
|
118
|
-
def filter_except(
|
119
|
-
return
|
142
|
+
def filter_except(cfg)
|
143
|
+
return cfg unless @controller
|
120
144
|
|
121
|
-
|
122
|
-
|
145
|
+
except_param = @controller.class.try(:native_serializer_except_query_param)
|
146
|
+
only_param = @controller.class.try(:native_serializer_only_query_param)
|
147
|
+
if except_param && except = @controller.request.query_parameters[except_param].presence
|
123
148
|
except = except.split(",").map(&:strip).map(&:to_sym)
|
124
149
|
|
125
150
|
unless except.empty?
|
126
|
-
# Duplicate the
|
127
|
-
|
151
|
+
# Duplicate the cfg to avoid mutating class state.
|
152
|
+
cfg = cfg.deep_dup
|
128
153
|
|
129
154
|
# Filter `only`, `except` (additive), `include`, and `methods`.
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
155
|
+
if cfg[:only]
|
156
|
+
cfg[:only] = self.class.filter_subcfg(cfg[:only], except: except)
|
157
|
+
else
|
158
|
+
cfg[:except] = self.class.filter_subcfg(cfg[:except], except: except, additive: true)
|
159
|
+
end
|
160
|
+
cfg[:include] = self.class.filter_subcfg(cfg[:include], except: except)
|
161
|
+
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], except: except)
|
162
|
+
end
|
163
|
+
elsif only_param && only = @controller.request.query_parameters[only_param].presence
|
164
|
+
only = only.split(",").map(&:strip).map(&:to_sym)
|
165
|
+
|
166
|
+
unless only.empty?
|
167
|
+
# Duplicate the cfg to avoid mutating class state.
|
168
|
+
cfg = cfg.deep_dup
|
169
|
+
|
170
|
+
# For the `except` part of the serializer, we need to append any columns not in `only`.
|
171
|
+
model = @controller.get_model
|
172
|
+
except_cols = model&.column_names&.map(&:to_sym)&.reject { |c| c.in?(only) }
|
173
|
+
|
174
|
+
# Filter `only`, `except` (additive), `include`, and `methods`.
|
175
|
+
if cfg[:only]
|
176
|
+
cfg[:only] = self.class.filter_subcfg(cfg[:only], only: only)
|
177
|
+
else
|
178
|
+
cfg[:except] = self.class.filter_subcfg(cfg[:except], except: except_cols, additive: true)
|
179
|
+
end
|
180
|
+
cfg[:include] = self.class.filter_subcfg(cfg[:include], only: only)
|
181
|
+
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], only: only)
|
134
182
|
end
|
135
183
|
end
|
136
184
|
|
137
|
-
return
|
185
|
+
return cfg
|
138
186
|
end
|
139
187
|
|
140
188
|
# Get the raw serializer config.
|
@@ -150,7 +198,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
150
198
|
end
|
151
199
|
|
152
200
|
# If the config wasn't determined, build a serializer config from model fields.
|
153
|
-
fields = @controller.
|
201
|
+
fields = @controller.get_fields if @controller
|
154
202
|
if fields
|
155
203
|
if @model
|
156
204
|
columns, methods = fields.partition { |f| f.in?(@model.column_names) }
|
@@ -168,7 +216,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
168
216
|
|
169
217
|
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
170
218
|
def get_serializer_config
|
171
|
-
return filter_except(self._get_raw_serializer_config)
|
219
|
+
return @_serializer_config ||= filter_except(self._get_raw_serializer_config)
|
172
220
|
end
|
173
221
|
|
174
222
|
def serialize(**kwargs)
|
data/lib/rest_framework/utils.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
module RESTFramework::Utils
|
2
|
+
HTTP_METHOD_ORDERING = %w(GET POST PUT PATCH DELETE)
|
3
|
+
|
2
4
|
# Helper to take extra_actions hash and convert to a consistent format:
|
3
5
|
# `{paths:, methods:, kwargs:}`.
|
4
6
|
def self.parse_extra_actions(extra_actions)
|
@@ -7,7 +9,7 @@ module RESTFramework::Utils
|
|
7
9
|
path = k
|
8
10
|
|
9
11
|
# Convert structure to path/methods/kwargs.
|
10
|
-
if v.is_a?(Hash) #
|
12
|
+
if v.is_a?(Hash) # Allow kwargs to be used to define path differently from the key.
|
11
13
|
v = v.symbolize_keys
|
12
14
|
|
13
15
|
# Ensure methods is an array.
|
@@ -35,30 +37,68 @@ module RESTFramework::Utils
|
|
35
37
|
end
|
36
38
|
end
|
37
39
|
|
38
|
-
# Helper to get the
|
39
|
-
def self.
|
40
|
-
application_routes.router.recognize(request)
|
41
|
-
|
42
|
-
|
40
|
+
# Helper to get the first route pattern which matches the given request.
|
41
|
+
def self.get_request_route(application_routes, request)
|
42
|
+
application_routes.router.recognize(request) { |route, _| return route }
|
43
|
+
end
|
44
|
+
|
45
|
+
# Helper to get the route pattern for a route, stripped of the `(:format)` segment.
|
46
|
+
def self.get_route_pattern(route)
|
47
|
+
return route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
|
48
|
+
end
|
49
|
+
|
50
|
+
# Helper to normalize a path pattern by replacing URL params with generic placeholder, and
|
51
|
+
# removing the `(.:format)` at the end.
|
52
|
+
def self.normalize_path(path)
|
53
|
+
return path.gsub("(.:format)", "").gsub(/:[0-9A-Za-z_-]+/, ":x")
|
43
54
|
end
|
44
55
|
|
45
|
-
# Helper for showing routes under a controller action
|
46
|
-
def self.get_routes(application_routes, request)
|
47
|
-
|
48
|
-
|
56
|
+
# Helper for showing routes under a controller action; used for the browsable API.
|
57
|
+
def self.get_routes(application_routes, request, current_route: nil)
|
58
|
+
current_route ||= self.get_request_route(application_routes, request)
|
59
|
+
current_path = current_route.path.spec.to_s
|
60
|
+
current_levels = current_path.count("/")
|
61
|
+
current_normalized_path = self.normalize_path(current_path)
|
49
62
|
|
50
|
-
# Return routes that match our current route subdomain/pattern, grouped by controller.
|
63
|
+
# Return routes that match our current route subdomain/pattern, grouped by controller. We
|
64
|
+
# precompute certain properties of the route for performance.
|
51
65
|
return application_routes.routes.map { |r|
|
66
|
+
path = r.path.spec.to_s
|
67
|
+
levels = path.count("/")
|
68
|
+
|
69
|
+
# Show link if the route is GET and our current route has all required URL params.
|
70
|
+
if r.verb == "GET" && r.path.required_names.length == current_route.path.required_names.length
|
71
|
+
show_link_args = current_route.path.required_names.map { |n| request.params[n] }.compact
|
72
|
+
else
|
73
|
+
show_link_args = nil
|
74
|
+
end
|
75
|
+
|
52
76
|
{
|
77
|
+
route: r,
|
53
78
|
verb: r.verb,
|
54
|
-
path:
|
55
|
-
|
79
|
+
path: path,
|
80
|
+
normalized_path: self.normalize_path(path),
|
81
|
+
relative_path: path.split("/")[current_levels..]&.join("/"),
|
56
82
|
controller: r.defaults[:controller].presence,
|
83
|
+
action: r.defaults[:action].presence,
|
57
84
|
subdomain: r.defaults[:subdomain].presence,
|
58
|
-
|
85
|
+
levels: levels,
|
86
|
+
show_link_args: show_link_args,
|
59
87
|
}
|
60
88
|
}.select { |r|
|
61
|
-
|
62
|
-
|
89
|
+
(
|
90
|
+
(!r[:subdomain] || r[:subdomain] == request.subdomain.presence) &&
|
91
|
+
r[:normalized_path].start_with?(current_normalized_path) &&
|
92
|
+
r[:controller] &&
|
93
|
+
r[:action]
|
94
|
+
)
|
95
|
+
}.sort_by { |r|
|
96
|
+
# Sort by levels first, so the routes matching closely with current request show first, then
|
97
|
+
# by the path, and finally by the HTTP verb.
|
98
|
+
[r[:levels], r[:path], HTTP_METHOD_ORDERING.index(r[:verb]) || 99]
|
99
|
+
}.group_by { |r| r[:controller] }.sort_by { |c, _r|
|
100
|
+
# Sort the controller groups by current controller first, then depth, then alphanumerically.
|
101
|
+
[request.params[:controller] == c ? 0 : 1, c.count("/"), c]
|
102
|
+
}.to_h
|
63
103
|
end
|
64
104
|
end
|
@@ -6,19 +6,25 @@ module RESTFramework
|
|
6
6
|
def self.get_version(skip_git: false)
|
7
7
|
# First, attempt to get the version from git.
|
8
8
|
unless skip_git
|
9
|
-
version = `git describe --dirty
|
9
|
+
version = `git describe --dirty 2>/dev/null`&.strip
|
10
10
|
return version unless !version || version.empty?
|
11
11
|
end
|
12
12
|
|
13
13
|
# Git failed or was skipped, so try to find a VERSION file.
|
14
14
|
begin
|
15
15
|
version = File.read(VERSION_FILEPATH)&.strip
|
16
|
-
return version unless !version || version.
|
16
|
+
return version unless !version || version.empty?
|
17
17
|
rescue SystemCallError
|
18
18
|
end
|
19
19
|
|
20
|
+
# If that fails, then try to get a plain commit SHA from git.
|
21
|
+
unless skip_git
|
22
|
+
version = `git describe --dirty --always`&.strip
|
23
|
+
return "0.#{version}" unless !version || version.empty?
|
24
|
+
end
|
25
|
+
|
20
26
|
# No VERSION file, so version is unknown.
|
21
|
-
return "unknown"
|
27
|
+
return "0.unknown"
|
22
28
|
end
|
23
29
|
|
24
30
|
def self.stamp_version
|
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.5.
|
4
|
+
version: 0.5.5
|
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: 2022-03-
|
11
|
+
date: 2022-03-15 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|