haveapi 0.28.0 → 0.28.2

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: 4dc49769be220f91aa34770fa280e1507a3eef56ad97f1cdda575a4d42f42ad0
4
- data.tar.gz: '09177e0cf827dbed3e8a31290c2f0088a7e7857bab9c4cbd2d3953a55a84747e'
3
+ metadata.gz: 1c0e9ead0f7cf99be75dc598c843487b2609b0cdc902694b800b9fcdb7df7ef9
4
+ data.tar.gz: 1689ebabcc73b7c9f31c53d328abf133f3ab6b3feb6c7892ddbfa1f32f7e426d
5
5
  SHA512:
6
- metadata.gz: 0bbac0d82fcfcba37da6874b54b667036cac326aead8da3e573ce1fac77a986fe191d3e6fb1c1754d65cb0fc5f7fb4d7c7d1d7f5fe329e0135a019fb00100561
7
- data.tar.gz: d031c935fffbb7ccd5f4262598fae87e93b8055d393576336d4afd5f4b14a3e19c77d024dcfede042d1e37ac72f8a5d79fc84e96dd94f2a5e0e74ca9984840d9
6
+ metadata.gz: 44f5728075305342ec0e01a35d80c9ec85a07552b15b44acb1a5ee8139017dd5a0c669ee415f2ecd9b8a66a4eff65a8968fde169d5cbec38e767a036e81cc97a
7
+ data.tar.gz: 40acf5edb198ccb57b8f195342c9aed5d8badbfb87f6e65fda2725f641d21ee1cd8f348d5393e99d12441f56015e0679c371a53f4ab25999fdb6ba592d322789
data/haveapi.gemspec CHANGED
@@ -15,7 +15,7 @@ Gem::Specification.new do |s|
15
15
  s.required_ruby_version = ">= #{File.read('../../.ruby-version').strip}"
16
16
 
17
17
  s.add_dependency 'activesupport', '>= 7.1'
18
- s.add_dependency 'haveapi-client', '~> 0.28.0'
18
+ s.add_dependency 'haveapi-client', '~> 0.28.2'
19
19
  s.add_dependency 'json'
20
20
  s.add_dependency 'mail'
21
21
  s.add_dependency 'nesty', '~> 1.0'
@@ -259,9 +259,9 @@ module HaveAPI
259
259
  end
260
260
 
261
261
  def from_context(c)
262
- ret = new(nil, c.version, c.params, nil, c)
262
+ ret = new(nil, c.version, c.path_params || c.path_params_from_args, c.input || c.params || {}, c)
263
263
  ret.instance_exec do
264
- @safe_params = @params.dup
264
+ @safe_input = self.class.input && input_from_params(raw_input_params(self.class.input))
265
265
  @authorization = c.authorization
266
266
  @current_user = c.current_user
267
267
  end
@@ -287,7 +287,9 @@ module HaveAPI
287
287
  params = {}
288
288
 
289
289
  path_param_names(path).each do |name|
290
- params[name] = values.shift.to_s
290
+ value = values.shift.to_s
291
+ params[name] = value
292
+ params[name.to_sym] = value
291
293
  end
292
294
 
293
295
  params
@@ -306,14 +308,12 @@ module HaveAPI
306
308
  end
307
309
  end
308
310
 
309
- def initialize(request, version, params, body, context)
311
+ def initialize(request, version, params, input_params, context)
310
312
  super()
311
313
  @request = request
312
314
  @version = version
313
315
  @route_params = params.dup
314
- @body = body || {}
315
- @params = params
316
- @params.update(@body)
316
+ @raw_input = input_params || {}
317
317
  @context = context
318
318
  @context.action = self.class
319
319
  @context.action_instance = self
@@ -333,25 +333,28 @@ module HaveAPI
333
333
  end
334
334
 
335
335
  def validate!
336
- @params = validate
336
+ validate
337
337
  rescue ValidationError => e
338
- error!(e.message, e.to_hash, http_status: 400)
338
+ opts = {}
339
+ status = @context.server.validation_error_http_status
340
+ opts[:http_status] = status if status
341
+
342
+ error!(e.message, e.to_hash, opts)
339
343
  end
340
344
 
341
345
  def authorized?(user)
342
346
  @current_user = user
343
- @authorization.authorized?(user, extract_path_params)
347
+ @authorization.authorized?(user, path_params)
344
348
  end
345
349
 
346
- def params
347
- @safe_params
350
+ def path_params
351
+ @path_params ||= extract_path_params
348
352
  end
349
353
 
350
354
  def input
351
355
  return unless self.class.input
352
356
 
353
- ns = self.class.input.namespace
354
- ns ? @safe_params[ns] : @safe_params
357
+ @safe_input
355
358
  end
356
359
 
357
360
  def meta
@@ -593,51 +596,37 @@ module HaveAPI
593
596
 
594
597
  def validate
595
598
  # Validate standard input
596
- @safe_params = @route_params.dup
599
+ @safe_input = nil
597
600
  input = self.class.input
598
601
 
599
602
  if input
600
- raw_params = @route_params.merge(@body)
603
+ raw_params = raw_input_params(input)
601
604
 
602
605
  # First check layout
603
606
  input.check_layout(raw_params)
604
607
 
605
608
  # Then filter allowed params
606
- case input.layout
607
- when :object_list, :hash_list
608
- filtered = raw_params[input.namespace].map do |obj|
609
- @authorization.filter_input(
610
- self.class.input.params,
611
- self.class.model_adapter(self.class.input.layout).input(obj)
612
- )
613
- end
614
- if input.namespace
615
- @safe_params[input.namespace] = filtered
616
- else
617
- @safe_params = filtered
618
- end
619
-
620
- else
621
- filtered = @authorization.filter_input(
622
- self.class.input.params,
623
- self.class.model_adapter(self.class.input.layout).input(
624
- input.namespace ? raw_params[input.namespace] : raw_params
625
- )
626
- )
627
- if input.namespace
628
- @safe_params[input.namespace] = filtered
629
- else
630
- self.class.input.params.each do |p|
631
- @safe_params.delete(p.name)
632
- @safe_params.delete(p.name.to_s)
633
- end
634
- @safe_params.update(filtered)
635
- end
636
- end
609
+ @safe_input = case input.layout
610
+ when :object_list, :hash_list
611
+ input_from_params(raw_params).map do |obj|
612
+ @authorization.filter_input(
613
+ self.class.input.params,
614
+ self.class.model_adapter(self.class.input.layout).input(obj)
615
+ )
616
+ end
617
+
618
+ else
619
+ @authorization.filter_input(
620
+ self.class.input.params,
621
+ self.class.model_adapter(self.class.input.layout).input(
622
+ input_from_params(raw_params)
623
+ )
624
+ )
625
+ end
637
626
 
638
627
  # Now check required params, convert types and set defaults
639
628
  input.validate(
640
- @safe_params,
629
+ validated_input_params(input),
641
630
  context: @context,
642
631
  only: @authorization.permitted_input_names(self.class.input.params)
643
632
  )
@@ -678,15 +667,42 @@ module HaveAPI
678
667
  def metadata_params(type, input)
679
668
  case type
680
669
  when :global
681
- fetch_metadata_from(@params)
670
+ fetch_metadata_from(@raw_input)
682
671
  when :object
683
672
  return unless input && input.namespace
684
673
 
685
- obj_params = @params[input.namespace]
674
+ obj_params = fetch_param(@raw_input, input.namespace)
686
675
  fetch_metadata_from(obj_params) if obj_params.is_a?(Hash)
687
676
  end
688
677
  end
689
678
 
679
+ def raw_input_params(input)
680
+ return @raw_input.dup unless input.namespace
681
+
682
+ { input.namespace => fetch_param(@raw_input, input.namespace) }
683
+ end
684
+
685
+ def validated_input_params(input)
686
+ input.namespace ? { input.namespace => @safe_input } : @safe_input
687
+ end
688
+
689
+ def input_from_params(params)
690
+ input = self.class.input
691
+ return unless input
692
+
693
+ input.namespace ? fetch_param(params, input.namespace) : params
694
+ end
695
+
696
+ def fetch_param(params, name)
697
+ return unless params
698
+ return params[name] if params.has_key?(name)
699
+
700
+ string_name = name.to_s
701
+ return params[string_name] if params.has_key?(string_name)
702
+
703
+ nil
704
+ end
705
+
690
706
  def fetch_metadata_from(params)
691
707
  [Metadata.namespace, Metadata.namespace.to_s].each do |ns|
692
708
  return params[ns] if params && params.has_key?(ns)
@@ -714,7 +730,7 @@ module HaveAPI
714
730
  global_meta = self.class.meta(:global)
715
731
  return @reply_meta[:global] unless global_meta && global_meta.output
716
732
 
717
- @authorization.filter_output(
733
+ @authorization.filter_meta_output(
718
734
  global_meta.output.params,
719
735
  self.class.model_adapter(global_meta.output.layout).output(@context, @reply_meta[:global]),
720
736
  true
@@ -242,6 +242,12 @@ module HaveAPI::Authentication
242
242
  allow
243
243
  end
244
244
 
245
+ def validate!
246
+ validate
247
+ rescue HaveAPI::ValidationError => e
248
+ error!(e.message, e.to_hash, http_status: 400)
249
+ end
250
+
245
251
  def exec
246
252
  config = self.class.resource.token_instance.config
247
253
 
@@ -378,6 +384,12 @@ module HaveAPI::Authentication
378
384
  allow
379
385
  end
380
386
 
387
+ def validate!
388
+ validate
389
+ rescue HaveAPI::ValidationError => e
390
+ error!(e.message, e.to_hash, http_status: 400)
391
+ end
392
+
381
393
  define_method(:exec) do
382
394
  begin
383
395
  result = config.handle.call(ActionRequest.new(
@@ -61,6 +61,13 @@ module HaveAPI
61
61
  }
62
62
  end
63
63
 
64
+ def meta_output(whitelist: nil, blacklist: nil)
65
+ @meta_output = {
66
+ whitelist:,
67
+ blacklist:
68
+ }
69
+ end
70
+
64
71
  def allow
65
72
  throw(:rule, true)
66
73
  end
@@ -91,6 +98,10 @@ module HaveAPI
91
98
  filter_inner(output, @output, params, format)
92
99
  end
93
100
 
101
+ def filter_meta_output(output, params, format = false)
102
+ filter_inner(output, meta_output_filter, params, format)
103
+ end
104
+
94
105
  def permitted_input_names(params)
95
106
  permitted_params(params, @input).map(&:name)
96
107
  end
@@ -128,6 +139,16 @@ module HaveAPI
128
139
  end
129
140
  end
130
141
 
142
+ def meta_output_filter
143
+ return @meta_output if @meta_output
144
+ return unless @output && @output[:blacklist]
145
+
146
+ {
147
+ whitelist: nil,
148
+ blacklist: @output[:blacklist]
149
+ }
150
+ end
151
+
131
152
  def normalize_names(names)
132
153
  names.map { |name| normalize_key(name) }
133
154
  end
@@ -2,13 +2,13 @@ module HaveAPI
2
2
  class Context
3
3
  attr_accessor :server, :version, :request, :resource, :action, :path, :args,
4
4
  :params, :current_user, :authorization, :endpoint, :resource_path,
5
- :action_instance, :action_prepare, :layout, :doc,
5
+ :path_params, :input, :action_instance, :action_prepare, :layout, :doc,
6
6
  :auth_users_by_version
7
7
 
8
8
  def initialize(server, version: nil, request: nil, resource: [], action: nil,
9
9
  path: nil, args: nil, params: nil, user: nil,
10
10
  authorization: nil, endpoint: nil, resource_path: [], doc: false,
11
- auth_users_by_version: nil)
11
+ auth_users_by_version: nil, path_params: nil, input: nil)
12
12
  @server = server
13
13
  @version = version
14
14
  @request = request
@@ -17,6 +17,8 @@ module HaveAPI
17
17
  @path = path
18
18
  @args = args
19
19
  @params = params
20
+ @path_params = path_params
21
+ @input = input
20
22
  @current_user = user
21
23
  @authorization = authorization
22
24
  @endpoint = endpoint
@@ -64,6 +66,19 @@ module HaveAPI
64
66
  ret
65
67
  end
66
68
 
69
+ def action_path_for(action, args = nil)
70
+ ret = @server.path_for_action(@version, action) || path_for(action)
71
+
72
+ ret = ret.dup
73
+ args.each { |arg| resolve_arg!(ret, arg) } if args
74
+
75
+ ret
76
+ end
77
+
78
+ def path_params_for(action, args)
79
+ action.path_params(action_path_for(action), args)
80
+ end
81
+
67
82
  def call_path_params(action, obj)
68
83
  ret = params && action.resolve_path_params(obj)
69
84
 
@@ -42,6 +42,7 @@ module HaveAPI
42
42
 
43
43
  def authorized?(context)
44
44
  return true unless @authorization
45
+ return true unless context.current_user
45
46
 
46
47
  @authorization.call(context.current_user) ? true : false
47
48
  end
@@ -227,8 +227,12 @@ module HaveAPI::Extensions
227
227
  <td><%=h context.args %></td>
228
228
  </tr>
229
229
  <tr>
230
- <th>Parameters</th>
231
- <td><%=h context.params %></td>
230
+ <th>Path parameters</th>
231
+ <td><%=h context.path_params %></td>
232
+ </tr>
233
+ <tr>
234
+ <th>Input</th>
235
+ <td><%=h context.input %></td>
232
236
  </tr>
233
237
  <tr>
234
238
  <th>User</th>
@@ -48,7 +48,7 @@ module HaveAPI
48
48
  def describe(context)
49
49
  {
50
50
  input: @input && @input.describe(context),
51
- output: @output && @output.describe(context)
51
+ output: @output && @output.describe(context, metadata: true)
52
52
  }
53
53
  end
54
54
  end
@@ -383,24 +383,29 @@ module HaveAPI::ModelAdapters
383
383
  push_cls = @context.action
384
384
  push_ins = @context.action_instance
385
385
  push_path = @context.path
386
- @context.path = res_show.build_route('')
386
+ push_path_params = @context.path_params
387
+ path = @context.action_path_for(res_show)
388
+ path_params = @context.path_params_for(res_show, args)
389
+ @context.path = path
390
+ @context.path_params = path_params
387
391
 
388
392
  res_show.new(
389
393
  push_ins.request,
390
394
  push_ins.version,
391
- res_show.path_params(@context.path, args),
395
+ path_params,
392
396
  nil,
393
397
  @context
394
398
  )
395
399
  yield @context.action_instance
396
400
  ensure
397
- restore_context(push_cls, push_ins, push_path)
401
+ restore_context(push_cls, push_ins, push_path, push_path_params)
398
402
  end
399
403
 
400
- def restore_context(action, action_instance, path)
404
+ def restore_context(action, action_instance, path, path_params)
401
405
  @context.action = action
402
406
  @context.action_instance = action_instance
403
407
  @context.path = path
408
+ @context.path_params = path_params
404
409
  end
405
410
 
406
411
  def show_prepared?(show)
@@ -159,19 +159,20 @@ module HaveAPI::Parameters
159
159
  return if record.nil? || context.nil?
160
160
  return unless show_action.authorization
161
161
 
162
- path = show_action.build_route('')
163
- path_params = show_action.path_params(path, show_action.resolve_path_params(record))
162
+ path = context.action_path_for(show_action)
163
+ path_params = context.path_params_for(show_action, show_action.resolve_path_params(record))
164
164
  child_context = HaveAPI::Context.new(
165
165
  context.server,
166
166
  version: context.version,
167
167
  request: context.request,
168
168
  action: show_action,
169
169
  path:,
170
- params: path_params,
170
+ path_params:,
171
+ input: {},
171
172
  user: context.current_user,
172
173
  endpoint: context.endpoint
173
174
  )
174
- action = show_action.new(context.request, context.version, path_params, nil, child_context)
175
+ action = show_action.new(context.request, context.version, path_params, {}, child_context)
175
176
  return if action.authorized?(context.current_user)
176
177
 
177
178
  raise HaveAPI::ValidationError, 'resource not found'
@@ -3,7 +3,10 @@ require 'time'
3
3
 
4
4
  module HaveAPI::Parameters
5
5
  class Typed
6
- ATTRIBUTES = %i[label desc type db_name default fill clean protected load_validators nullable].freeze
6
+ ATTRIBUTES = %i[
7
+ label desc type db_name default fill clean protected load_validators
8
+ nullable symbolize_keys
9
+ ].freeze
7
10
 
8
11
  attr_reader :name, :label, :desc, :type, :default
9
12
 
@@ -79,7 +82,8 @@ module HaveAPI::Parameters
79
82
  end
80
83
 
81
84
  def clean(raw)
82
- return validate_cleaned_value(instance_exec(raw, &@clean)) if @clean
85
+ clean_raw = custom? ? normalize_custom_keys(raw) : raw
86
+ return validate_cleaned_value(instance_exec(clean_raw, &@clean)) if @clean
83
87
 
84
88
  if raw.nil?
85
89
  return nil if nullable?
@@ -110,6 +114,9 @@ module HaveAPI::Parameters
110
114
  elsif @type == String || @type == Text
111
115
  coerce_string(raw)
112
116
 
117
+ elsif custom?
118
+ clean_raw
119
+
113
120
  else
114
121
  raw
115
122
  end
@@ -153,6 +160,31 @@ module HaveAPI::Parameters
153
160
  value
154
161
  end
155
162
 
163
+ def custom?
164
+ @type == Custom
165
+ end
166
+
167
+ def normalize_custom_keys(value)
168
+ case value
169
+ when ::Hash
170
+ value.each_with_object({}) do |(key, inner), ret|
171
+ ret[normalize_custom_key(key)] = normalize_custom_keys(inner)
172
+ end
173
+ when ::Array
174
+ value.map { |inner| normalize_custom_keys(inner) }
175
+ else
176
+ value
177
+ end
178
+ end
179
+
180
+ def normalize_custom_key(key)
181
+ if @symbolize_keys
182
+ key.respond_to?(:to_sym) ? key.to_sym : key.to_s.to_sym
183
+ else
184
+ key.to_s
185
+ end
186
+ end
187
+
156
188
  def strip_string(value)
157
189
  value.strip
158
190
  rescue ArgumentError, Encoding::CompatibilityError
@@ -167,11 +167,11 @@ module HaveAPI
167
167
  end
168
168
 
169
169
  # Action returns custom data.
170
- def custom(name, **kwargs, &block)
171
- add_param(name, apply(kwargs, type: Custom, clean: block))
170
+ def custom(name, symbolize_keys: false, **kwargs, &block)
171
+ add_param(name, apply(kwargs, type: Custom, clean: block, symbolize_keys:))
172
172
  end
173
173
 
174
- def describe(context)
174
+ def describe(context, metadata: false)
175
175
  context.layout = layout
176
176
 
177
177
  ret = { parameters: {} }
@@ -183,17 +183,7 @@ module HaveAPI
183
183
  ret[:parameters][p.name] = p.describe(context)
184
184
  end
185
185
 
186
- ret[:parameters] = if @direction == :input
187
- context.authorization.filter_input(
188
- @params,
189
- ModelAdapters::Hash.output(context, ret[:parameters])
190
- )
191
- else
192
- context.authorization.filter_output(
193
- @params,
194
- ModelAdapters::Hash.output(context, ret[:parameters])
195
- )
196
- end
186
+ ret[:parameters] = filtered_description_parameters(context, ret, metadata)
197
187
 
198
188
  ret
199
189
  end
@@ -293,6 +283,18 @@ module HaveAPI
293
283
 
294
284
  private
295
285
 
286
+ def filtered_description_parameters(context, ret, metadata)
287
+ params = ModelAdapters::Hash.output(context, ret[:parameters])
288
+
289
+ if @direction == :input
290
+ context.authorization.filter_input(@params, params)
291
+ elsif metadata
292
+ context.authorization.filter_meta_output(@params, params)
293
+ else
294
+ context.authorization.filter_output(@params, params)
295
+ end
296
+ end
297
+
296
298
  def add_param(name, kwargs)
297
299
  p = Parameters::Typed.new(name, kwargs)
298
300
 
@@ -111,7 +111,7 @@ module HaveAPI::Resources
111
111
  loop do
112
112
  state = @context.server.action_state.new(
113
113
  current_user,
114
- id: params[:action_state_id]
114
+ id: path_params['action_state_id']
115
115
  )
116
116
 
117
117
  error!('action state not found') unless state.valid?
@@ -148,7 +148,7 @@ module HaveAPI::Resources
148
148
  def exec
149
149
  state = @context.server.action_state.new(
150
150
  current_user,
151
- id: params[:action_state_id]
151
+ id: path_params['action_state_id']
152
152
  )
153
153
 
154
154
  return state_to_hash(state) if state.valid?
@@ -169,7 +169,7 @@ module HaveAPI::Resources
169
169
  def exec
170
170
  state = @context.server.action_state.new(
171
171
  current_user,
172
- id: params[:action_state_id]
172
+ id: path_params['action_state_id']
173
173
  )
174
174
 
175
175
  error!('action state not found') unless state.valid?
@@ -6,8 +6,9 @@ require 'haveapi/hooks'
6
6
 
7
7
  module HaveAPI
8
8
  class Server
9
- attr_accessor :default_version, :action_state
10
- attr_reader :root, :routes, :module_name, :auth_chain, :versions, :extensions
9
+ attr_accessor :default_version, :action_state, :validation_error_http_status
10
+ attr_reader :root, :routes, :module_name, :auth_chain, :versions, :extensions,
11
+ :action_state_auth
11
12
 
12
13
  include Hookable
13
14
 
@@ -220,6 +221,19 @@ module HaveAPI
220
221
  @allowed_headers = ['Content-Type']
221
222
  @auth_chain = HaveAPI::Authentication::Chain.new(self)
222
223
  @extensions = []
224
+ @action_state_auth = :backend
225
+ @validation_error_http_status = nil
226
+ end
227
+
228
+ def action_state_auth=(mode)
229
+ @action_state_auth = case mode
230
+ when :backend, false, nil
231
+ :backend
232
+ when :required, true
233
+ :required
234
+ else
235
+ raise ArgumentError, "unsupported action_state_auth #{mode.inspect}"
236
+ end
223
237
  end
224
238
 
225
239
  # Include specific version `v` of API.
@@ -530,7 +544,7 @@ module HaveAPI
530
544
  end
531
545
 
532
546
  begin
533
- body = raw_body.empty? ? nil : JSON.parse(raw_body, symbolize_names: true)
547
+ body = raw_body.empty? ? nil : JSON.parse(raw_body)
534
548
  rescue JSON::ParserError
535
549
  report_error(400, {}, 'Bad JSON syntax')
536
550
  end
@@ -539,8 +553,8 @@ module HaveAPI
539
553
  report_error(400, {}, 'JSON body must be an object')
540
554
  end
541
555
 
542
- action_params = body_method ? settings.api_server.send(:path_params, route, params) : params
543
- context_params = body ? action_params.merge(body) : action_params
556
+ action_params = settings.api_server.send(:path_params, route, params)
557
+ action_input = body_method ? (body || {}) : request.GET
544
558
 
545
559
  context = Context.new(
546
560
  settings.api_server,
@@ -548,13 +562,14 @@ module HaveAPI
548
562
  request: self,
549
563
  action: route.action,
550
564
  path: route.path,
551
- params: context_params,
565
+ path_params: action_params,
566
+ input: action_input,
552
567
  user: current_user,
553
568
  endpoint: true,
554
569
  resource_path: route.resource_path
555
570
  )
556
571
 
557
- action = route.action.new(request, v, action_params, body, context)
572
+ action = route.action.new(request, v, action_params, action_input, context)
558
573
 
559
574
  unless action.authorized?(current_user)
560
575
  report_error(403, {}, 'Access denied. Insufficient permissions.')
@@ -674,10 +689,18 @@ module HaveAPI
674
689
  r.describe(hash, context)
675
690
  end
676
691
 
692
+ def path_for_action(version, action)
693
+ routes = @routes && @routes[version]
694
+ return unless routes
695
+
696
+ find_action_path(routes[:resources], action)
697
+ end
698
+
677
699
  def action_state_auth_required?(route)
700
+ return false unless route.action.resource == HaveAPI::Resources::ActionState
678
701
  return false if @auth_chain.empty?
679
702
 
680
- route.action.resource == HaveAPI::Resources::ActionState
703
+ @action_state_auth == :required
681
704
  end
682
705
 
683
706
  def version_prefix(v)
@@ -753,5 +776,17 @@ module HaveAPI
753
776
  def do_authenticate(v, request)
754
777
  @auth_chain.authenticate(v, request)
755
778
  end
779
+
780
+ def find_action_path(resources, action)
781
+ resources.each_value do |node|
782
+ path = node[:actions][action]
783
+ return path if path
784
+
785
+ path = find_action_path(node[:resources], action)
786
+ return path if path
787
+ end
788
+
789
+ nil
790
+ end
756
791
  end
757
792
  end