rest_framework 0.5.2 → 0.5.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6e9dae13c73c724a592cf7f9f99347eae18dcc4acd135e98bcaf5f8682ea01e3
4
- data.tar.gz: 52b5a7b3a0b3339987516e6ec2083c087b9387ceb13a2951523c339ee1509b2c
3
+ metadata.gz: 237449a323a14356f20e47cd2cbab3591aa749fc7e6f43bc06587995ff1041dc
4
+ data.tar.gz: 98ff07d840fca2fd09ab608a08a2814c5fe17c9ded1a7476c176941224c75e53
5
5
  SHA512:
6
- metadata.gz: 8c9d79bd771ab07ffbd25edcc9970a3cc4ab39dfe730330e4fd704737773f7381bc75fd3f6169a9a6c17653cd3853da45a56a46907b05be668b63f8e921ee482
7
- data.tar.gz: 9e5ec3f519a35f02f49d2fde38f3f793649361927e437bc243ff7eba5a38a8315c4557b001a4eb305aeec04e99fb91ea00727aa910e5f33496ea58d29ad28598
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
- $ rake test
123
+ $ rails test
124
124
  ```
125
125
 
126
- To interact with the test app, `cd test` and operate it via the normal Rails interfaces. Ensure you
127
- run `rake db:schema:load` before running `rails server` or `rails console`. You can also load the
128
- test fixtures with `rake db:fixtures:load`.
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.2
1
+ 0.5.5
@@ -1,9 +1,11 @@
1
1
  <tr>
2
- <td><%= route[:path] %></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
- <% if route[:controller] && route[:action] %>
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>
@@ -1,5 +1,5 @@
1
1
  <div class="table-responsive">
2
- <table class="table table-responsive rrf-routes">
2
+ <table class="table table-sm rrf-routes">
3
3
  <thead>
4
4
  <tr>
5
5
  <th scope="col">Path</th>
@@ -81,8 +81,6 @@ module RESTFramework::BaseControllerMixin
81
81
  end
82
82
  end
83
83
 
84
- protected
85
-
86
84
  # Helper to get the configured serializer class.
87
85
  def get_serializer_class
88
86
  return nil unless serializer_class = self.class.serializer_class
@@ -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.send(:get_filterset_fields)
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.send(:get_ordering_fields)
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.send(:get_search_fields)
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
- protected def _get_controller_class(name, pluralize: true, fallback_reverse_pluralization: true)
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
- protected def _route_extra_actions(actions, &block)
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
- protected def _rest_resources(default_singular, name, skip_undefined: true, **kwargs, &block)
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.send(:get_model) if @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.filter_subconfig(subconfig, except, additive: false)
88
- return subconfig if !subconfig && !additive
89
-
90
- if subconfig.is_a?(Array)
91
- subconfig = subconfig.map(&:to_sym)
92
- if additive
93
- # Only add fields which are not already included.
94
- subconfig += except - subconfig
95
- else
96
- subconfig -= except
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 subconfig.is_a?(Hash)
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
- subconfig.symbolize_keys!
102
- subconfig.reject! { |k, _v| k.in?(except) }
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 !subconfig
105
- else # Subconfig is a single element (assume string/symbol).
106
- subconfig = subconfig.to_sym
107
- if subconfig.in?(except)
108
- subconfig = [] unless additive
109
- elsif additive
110
- subconfig = [subconfig, *except]
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 subconfig
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(config)
119
- return config unless @controller
142
+ def filter_except(cfg)
143
+ return cfg unless @controller
120
144
 
121
- except_query_param = @controller.class.try(:native_serializer_except_query_param)
122
- if except = @controller.request.query_parameters[except_query_param].presence
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 config to avoid mutating class state.
127
- config = config.deep_dup
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
- config[:only] = self.class.filter_subconfig(config[:only], except)
131
- config[:except] = self.class.filter_subconfig(config[:except], except, additive: true)
132
- config[:include] = self.class.filter_subconfig(config[:include], except)
133
- config[:methods] = self.class.filter_subconfig(config[:methods], except)
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 config
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.send(:get_fields) if @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)
@@ -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) # allow kwargs
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 current route pattern, stripped of the `(:format)` segment.
39
- def self.get_route_pattern(application_routes, request)
40
- application_routes.router.recognize(request) do |route, _, _|
41
- return route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
42
- end
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, used for the browsable API.
46
- def self.get_routes(application_routes, request)
47
- current_pattern = self.get_route_pattern(application_routes, request)
48
- current_subdomain = request.subdomain.presence
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: r.path.spec.to_s,
55
- action: r.defaults[:action].presence,
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
- route_app: r.app&.app&.inspect&.presence,
85
+ levels: levels,
86
+ show_link_args: show_link_args,
59
87
  }
60
88
  }.select { |r|
61
- r[:subdomain] == current_subdomain && r[:path].start_with?(current_pattern)
62
- }.group_by { |r| r[:controller] }
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 --broken 2>/dev/null`&.strip
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.blank?
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.2
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-09 00:00:00.000000000 Z
11
+ date: 2022-03-15 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails