rest_framework 0.5.3 → 0.5.4

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: 3a1ec41fbc6d06700c4eb62d33369bf489af800914e92c304465d698ef788886
4
- data.tar.gz: cc12e9c7e80fd882360f33d00e3ba2dd0f9b415ca7464d1db748afc3c6eec01c
3
+ metadata.gz: 413ee55f2eecbb8a8cdd3b133852d83a3e3fdb7e43f19c8deae12adcb3d38977
4
+ data.tar.gz: 6582a4d8acd988ad3034af3191b75454fcf602c5f968772e18fa9d5535b71dea
5
5
  SHA512:
6
- metadata.gz: 56eac9f7fa7fa5daeb33cbc2b594ac96ba3ca6e27faf2ca10da7aac0aa3508f2fdcf8252b3af35ca293b504630741b3323f0f0f87ad65cecd8b01baf89fdf09e
7
- data.tar.gz: 3b46a24e428d32b431b189386beb6ed41e898fc22ac0a470718143d56d86a04ec9a5b26bbdebb73874aae9d4aa8bdc6349b25c485ad34a752d57defed067b95b
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
- $ 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.3
1
+ 0.5.4
@@ -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
@@ -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.send(:get_model) if @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.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
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 subconfig.is_a?(Hash)
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
- subconfig.symbolize_keys!
102
- subconfig.reject! { |k, _v| k.in?(except) }
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 !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]
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 subconfig
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(config)
119
- return config unless @controller
140
+ def filter_except(cfg)
141
+ return cfg unless @controller
120
142
 
121
- except_query_param = @controller.class.try(:native_serializer_except_query_param)
122
- if except = @controller.request.query_parameters[except_query_param].presence
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 config to avoid mutating class state.
127
- config = config.deep_dup
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
- 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)
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 config
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.send(:get_fields) if @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)
@@ -37,34 +37,68 @@ module RESTFramework::Utils
37
37
  end
38
38
  end
39
39
 
40
- # Helper to get the current route pattern, stripped of the `(:format)` segment.
41
- def self.get_route_pattern(application_routes, request)
42
- application_routes.router.recognize(request) do |route, _, _|
43
- return route.path.spec.to_s.gsub(/\(\.:format\)$/, "")
44
- 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\)$/, "")
45
48
  end
46
49
 
47
- # Helper for showing routes under a controller action, used for the browsable API.
48
- def self.get_routes(application_routes, request)
49
- current_pattern = self.get_route_pattern(application_routes, request)
50
- current_subdomain = request.subdomain.presence
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")
54
+ end
55
+
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)
51
62
 
52
- # 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.
53
65
  return application_routes.routes.map { |r|
54
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
+
55
76
  {
77
+ route: r,
56
78
  verb: r.verb,
57
79
  path: path,
58
- action: r.defaults[:action].presence,
80
+ normalized_path: self.normalize_path(path),
81
+ relative_path: path.split("/")[current_levels..]&.join("/"),
59
82
  controller: r.defaults[:controller].presence,
83
+ action: r.defaults[:action].presence,
60
84
  subdomain: r.defaults[:subdomain].presence,
61
- route_app: r.app&.app&.inspect&.presence,
62
- _levels: path.count("/"),
85
+ levels: levels,
86
+ show_link_args: show_link_args,
63
87
  }
64
88
  }.select { |r|
65
- r[:subdomain] == current_subdomain && r[:path].start_with?(current_pattern)
89
+ (
90
+ r[:subdomain] == request.subdomain.presence &&
91
+ r[:normalized_path].start_with?(current_normalized_path) &&
92
+ r[:controller] &&
93
+ r[:action]
94
+ )
66
95
  }.sort_by { |r|
67
- [r[:_levels], r[:path], HTTP_METHOD_ORDERING.index(r[:verb]) || 99]
68
- }.group_by { |r| r[:controller] }
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
69
103
  end
70
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.3
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-10 00:00:00.000000000 Z
11
+ date: 2022-03-14 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails