rest_framework 0.5.3 → 0.5.4

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