haveapi 0.28.1 → 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: 57ff88678f46548f5a2717e7aae799df778723b95b0384471e873b17b0791669
4
- data.tar.gz: 2062ffb0fef65373b74d7ecef1eae40c0cf16433eef60d3d28c52c3b8b4c7e9e
3
+ metadata.gz: 1c0e9ead0f7cf99be75dc598c843487b2609b0cdc902694b800b9fcdb7df7ef9
4
+ data.tar.gz: 1689ebabcc73b7c9f31c53d328abf133f3ab6b3feb6c7892ddbfa1f32f7e426d
5
5
  SHA512:
6
- metadata.gz: 5c9d3bc013ac270b3de0b411838a69ac20c920eef8d11cf7552da3e9a852328ab493a5642fc3d4490e7d126d8114f6eda38269b06da2ad3f32a711fa673cbdb3
7
- data.tar.gz: 3deaf826dbc931825edd8babd1b453faf9521015bf07450d24e2c290bd37cbd0f47cf2dbde90e2245cf5cefa4c673723c97283c1c836f0826cd517117ad44500
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.1'
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
@@ -308,14 +308,12 @@ module HaveAPI
308
308
  end
309
309
  end
310
310
 
311
- def initialize(request, version, params, body, context)
311
+ def initialize(request, version, params, input_params, context)
312
312
  super()
313
313
  @request = request
314
314
  @version = version
315
315
  @route_params = params.dup
316
- @body = body || {}
317
- @params = params
318
- @params.update(@body)
316
+ @raw_input = input_params || {}
319
317
  @context = context
320
318
  @context.action = self.class
321
319
  @context.action_instance = self
@@ -335,25 +333,28 @@ module HaveAPI
335
333
  end
336
334
 
337
335
  def validate!
338
- @params = validate
336
+ validate
339
337
  rescue ValidationError => e
340
- 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)
341
343
  end
342
344
 
343
345
  def authorized?(user)
344
346
  @current_user = user
345
- @authorization.authorized?(user, extract_path_params)
347
+ @authorization.authorized?(user, path_params)
346
348
  end
347
349
 
348
- def params
349
- @safe_params
350
+ def path_params
351
+ @path_params ||= extract_path_params
350
352
  end
351
353
 
352
354
  def input
353
355
  return unless self.class.input
354
356
 
355
- ns = self.class.input.namespace
356
- ns ? @safe_params[ns] : @safe_params
357
+ @safe_input
357
358
  end
358
359
 
359
360
  def meta
@@ -595,51 +596,37 @@ module HaveAPI
595
596
 
596
597
  def validate
597
598
  # Validate standard input
598
- @safe_params = @route_params.dup
599
+ @safe_input = nil
599
600
  input = self.class.input
600
601
 
601
602
  if input
602
- raw_params = @route_params.merge(@body)
603
+ raw_params = raw_input_params(input)
603
604
 
604
605
  # First check layout
605
606
  input.check_layout(raw_params)
606
607
 
607
608
  # Then filter allowed params
608
- case input.layout
609
- when :object_list, :hash_list
610
- filtered = raw_params[input.namespace].map do |obj|
611
- @authorization.filter_input(
612
- self.class.input.params,
613
- self.class.model_adapter(self.class.input.layout).input(obj)
614
- )
615
- end
616
- if input.namespace
617
- @safe_params[input.namespace] = filtered
618
- else
619
- @safe_params = filtered
620
- end
621
-
622
- else
623
- filtered = @authorization.filter_input(
624
- self.class.input.params,
625
- self.class.model_adapter(self.class.input.layout).input(
626
- input.namespace ? raw_params[input.namespace] : raw_params
627
- )
628
- )
629
- if input.namespace
630
- @safe_params[input.namespace] = filtered
631
- else
632
- self.class.input.params.each do |p|
633
- @safe_params.delete(p.name)
634
- @safe_params.delete(p.name.to_s)
635
- end
636
- @safe_params.update(filtered)
637
- end
638
- 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
639
626
 
640
627
  # Now check required params, convert types and set defaults
641
628
  input.validate(
642
- @safe_params,
629
+ validated_input_params(input),
643
630
  context: @context,
644
631
  only: @authorization.permitted_input_names(self.class.input.params)
645
632
  )
@@ -680,15 +667,42 @@ module HaveAPI
680
667
  def metadata_params(type, input)
681
668
  case type
682
669
  when :global
683
- fetch_metadata_from(@params)
670
+ fetch_metadata_from(@raw_input)
684
671
  when :object
685
672
  return unless input && input.namespace
686
673
 
687
- obj_params = @params[input.namespace]
674
+ obj_params = fetch_param(@raw_input, input.namespace)
688
675
  fetch_metadata_from(obj_params) if obj_params.is_a?(Hash)
689
676
  end
690
677
  end
691
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
+
692
706
  def fetch_metadata_from(params)
693
707
  [Metadata.namespace, Metadata.namespace.to_s].each do |ns|
694
708
  return params[ns] if params && params.has_key?(ns)
@@ -716,7 +730,7 @@ module HaveAPI
716
730
  global_meta = self.class.meta(:global)
717
731
  return @reply_meta[:global] unless global_meta && global_meta.output
718
732
 
719
- @authorization.filter_output(
733
+ @authorization.filter_meta_output(
720
734
  global_meta.output.params,
721
735
  self.class.model_adapter(global_meta.output.layout).output(@context, @reply_meta[:global]),
722
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