rest_framework 0.5.1 → 0.5.4
Sign up to get free protection for your applications and to get access to all the features.
- 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 +4 -4
- data/lib/rest_framework/serializers.rb +81 -35
- data/lib/rest_framework/utils.rb +55 -15
- 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: 413ee55f2eecbb8a8cdd3b133852d83a3e3fdb7e43f19c8deae12adcb3d38977
|
4
|
+
data.tar.gz: 6582a4d8acd988ad3034af3191b75454fcf602c5f968772e18fa9d5535b71dea
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 19cb82822923617face6c76e0fd36cfa001f23e88393210990842563b0483891dab03638e11c65bf1182db6ebe0d164cf09bb3e35a7ea06b9769c729e16b85d5
|
7
|
+
data.tar.gz: fc853858e022b126d67f878c63f2e46e18a1f03d03e2aeda90f7c8996b6a461bd41d41d44fadb59d46256aa9c35b18efc1c5e9b7970f712dce010af0c9178e68
|
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.4
|
@@ -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
|
@@ -18,7 +18,7 @@ module ActionDispatch::Routing
|
|
18
18
|
|
19
19
|
# Get scope for the class.
|
20
20
|
if @scope[:module]
|
21
|
-
mod = @scope[:module].to_s.
|
21
|
+
mod = @scope[:module].to_s.camelize.constantize
|
22
22
|
else
|
23
23
|
mod = Object
|
24
24
|
end
|
@@ -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
|
@@ -43,7 +43,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
43
43
|
@model ||= @object[0].class if
|
44
44
|
@many && @object.is_a?(Enumerable) && @object.is_a?(ActiveRecord::Base)
|
45
45
|
|
46
|
-
@model ||= @controller.
|
46
|
+
@model ||= @controller.get_model if @controller
|
47
47
|
end
|
48
48
|
|
49
49
|
# Get controller action, if possible.
|
@@ -84,57 +84,103 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
84
84
|
end
|
85
85
|
|
86
86
|
# Helper to filter (mutate) a single subconfig for specific keys.
|
87
|
-
def self.
|
88
|
-
return
|
89
|
-
|
90
|
-
if
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
87
|
+
def self.filter_subcfg(subcfg, except: nil, only: nil, additive: false)
|
88
|
+
return subcfg unless except || only
|
89
|
+
return subcfg unless subcfg || additive
|
90
|
+
raise "Cannot pass `only` and `additive` to filter_subcfg." if only && additive
|
91
|
+
|
92
|
+
if subcfg.is_a?(Array)
|
93
|
+
subcfg = subcfg.map(&:to_sym)
|
94
|
+
if except
|
95
|
+
if additive
|
96
|
+
# Only add fields which are not already included.
|
97
|
+
subcfg += except - subcfg
|
98
|
+
else
|
99
|
+
subcfg -= except
|
100
|
+
end
|
101
|
+
elsif only
|
102
|
+
# Ignore `additive` in an `only` context, since it could causing data leaking.
|
103
|
+
unless additive
|
104
|
+
subcfg.select! { |c| c.in?(only) }
|
105
|
+
end
|
97
106
|
end
|
98
|
-
elsif
|
107
|
+
elsif subcfg.is_a?(Hash)
|
99
108
|
# Additive doesn't make sense in a hash context since we wouldn't know the values.
|
100
109
|
unless additive
|
101
|
-
|
102
|
-
|
110
|
+
if except
|
111
|
+
subcfg.symbolize_keys!
|
112
|
+
subcfg.reject! { |k, _v| k.in?(except) }
|
113
|
+
elsif only
|
114
|
+
subcfg.symbolize_keys!
|
115
|
+
subcfg.select! { |k, _v| k.in?(only) }
|
116
|
+
end
|
103
117
|
end
|
104
|
-
elsif !
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
118
|
+
elsif !subcfg
|
119
|
+
if additive && except
|
120
|
+
subcfg = except
|
121
|
+
end
|
122
|
+
else # Subcfg is a single element (assume string/symbol).
|
123
|
+
subcfg = subcfg.to_sym
|
124
|
+
|
125
|
+
if except
|
126
|
+
if subcfg.in?(except)
|
127
|
+
subcfg = [] unless additive
|
128
|
+
elsif additive
|
129
|
+
subcfg = [subcfg, *except]
|
130
|
+
end
|
131
|
+
elsif only && !additive && !subcfg.in?(only) # Protect only/additive data-leaking.
|
132
|
+
subcfg = []
|
111
133
|
end
|
112
134
|
end
|
113
135
|
|
114
|
-
return
|
136
|
+
return subcfg
|
115
137
|
end
|
116
138
|
|
117
139
|
# Helper to filter out configuration properties based on the :except query parameter.
|
118
|
-
def filter_except(
|
119
|
-
return
|
140
|
+
def filter_except(cfg)
|
141
|
+
return cfg unless @controller
|
120
142
|
|
121
|
-
|
122
|
-
|
143
|
+
except_param = @controller.class.try(:native_serializer_except_query_param)
|
144
|
+
only_param = @controller.class.try(:native_serializer_only_query_param)
|
145
|
+
if except_param && except = @controller.request.query_parameters[except_param].presence
|
123
146
|
except = except.split(",").map(&:strip).map(&:to_sym)
|
124
147
|
|
125
148
|
unless except.empty?
|
126
|
-
# Duplicate the
|
127
|
-
|
149
|
+
# Duplicate the cfg to avoid mutating class state.
|
150
|
+
cfg = cfg.deep_dup
|
128
151
|
|
129
152
|
# Filter `only`, `except` (additive), `include`, and `methods`.
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
153
|
+
if cfg[:only]
|
154
|
+
cfg[:only] = self.class.filter_subcfg(cfg[:only], except: except)
|
155
|
+
else
|
156
|
+
cfg[:except] = self.class.filter_subcfg(cfg[:except], except: except, additive: true)
|
157
|
+
end
|
158
|
+
cfg[:include] = self.class.filter_subcfg(cfg[:include], except: except)
|
159
|
+
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], except: except)
|
160
|
+
end
|
161
|
+
elsif only_param && only = @controller.request.query_parameters[only_param].presence
|
162
|
+
only = only.split(",").map(&:strip).map(&:to_sym)
|
163
|
+
|
164
|
+
unless only.empty?
|
165
|
+
# Duplicate the cfg to avoid mutating class state.
|
166
|
+
cfg = cfg.deep_dup
|
167
|
+
|
168
|
+
# For the `except` part of the serializer, we need to append any columns not in `only`.
|
169
|
+
model = @controller.get_model
|
170
|
+
except_cols = model&.column_names&.map(&:to_sym)&.reject { |c| c.in?(only) }
|
171
|
+
|
172
|
+
# Filter `only`, `except` (additive), `include`, and `methods`.
|
173
|
+
if cfg[:only]
|
174
|
+
cfg[:only] = self.class.filter_subcfg(cfg[:only], only: only)
|
175
|
+
else
|
176
|
+
cfg[:except] = self.class.filter_subcfg(cfg[:except], except: except_cols, additive: true)
|
177
|
+
end
|
178
|
+
cfg[:include] = self.class.filter_subcfg(cfg[:include], only: only)
|
179
|
+
cfg[:methods] = self.class.filter_subcfg(cfg[:methods], only: only)
|
134
180
|
end
|
135
181
|
end
|
136
182
|
|
137
|
-
return
|
183
|
+
return cfg
|
138
184
|
end
|
139
185
|
|
140
186
|
# Get the raw serializer config.
|
@@ -150,7 +196,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
150
196
|
end
|
151
197
|
|
152
198
|
# If the config wasn't determined, build a serializer config from model fields.
|
153
|
-
fields = @controller.
|
199
|
+
fields = @controller.get_fields if @controller
|
154
200
|
if fields
|
155
201
|
if @model
|
156
202
|
columns, methods = fields.partition { |f| f.in?(@model.column_names) }
|
@@ -168,7 +214,7 @@ class RESTFramework::NativeSerializer < RESTFramework::BaseSerializer
|
|
168
214
|
|
169
215
|
# Get a configuration passable to `serializable_hash` for the object, filtered if required.
|
170
216
|
def get_serializer_config
|
171
|
-
return filter_except(self._get_raw_serializer_config)
|
217
|
+
return @_serializer_config ||= filter_except(self._get_raw_serializer_config)
|
172
218
|
end
|
173
219
|
|
174
220
|
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)
|
@@ -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] == 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.4
|
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-14 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rails
|