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 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