omf_sfa 0.2.6 → 0.2.7

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: c8e057cf74d9cfbe634681eb5a86a56d1b06f48e
4
+ data.tar.gz: df5ac1030bab27d3db06ff6bd67e72c899ae75ee
5
+ SHA512:
6
+ metadata.gz: 6e561cdb795206908e2dc0f32cc58be10af3e5cb959bfafdf89f6b3be285ee470e92c46ce012b11dad2749ae998356aab1576fc71cffcc80272104eeb3100c59
7
+ data.tar.gz: dfbc2f4d3e40a9c4895c7ea6479d3e5dc7cd2a7df030522ea257aac78a8649e856745130048455501c067b031a70359800a1380df97d6bf79bd79a720082d1c9
data/.gitignore CHANGED
@@ -22,3 +22,4 @@ Gemfile.lock
22
22
  .project
23
23
  .DS_Store
24
24
  *.original
25
+ .idea
File without changes
@@ -3,5 +3,8 @@
3
3
  require 'omf_sfa'
4
4
 
5
5
  module OMF::SFA
6
- module AM; end
6
+ module AM
7
+ class AMException < Exception; end
8
+ end
7
9
  end
10
+
@@ -0,0 +1,119 @@
1
+
2
+ module OMF::SFA::AM::Rest
3
+
4
+ class PromiseHandler < RestHandler
5
+
6
+ @@contexts = {}
7
+
8
+ def self.register_promise(promise, uuid = nil, html_reply = false, collection_set = nil)
9
+ debug "Registering promise '#{uuid}' - #{promise}"
10
+ uuid ||= UUIDTools::UUID.random_create
11
+ #uuid = 1 # TODO: REMOVE
12
+ @@contexts[uuid.to_s] = {
13
+ promise: promise,
14
+ html_reply: html_reply,
15
+ collection_set: collection_set || Set.new,
16
+ timestamp: Time.now
17
+ }
18
+ path = "/promises/#{uuid}"
19
+ debug "Redirecting to #{path}"
20
+ return [302, {'Location' => path}, ['Promised, but not ready yet.']]
21
+ end
22
+
23
+ def call(env)
24
+ begin
25
+ req = ::Rack::Request.new(env)
26
+ headers = {
27
+ 'Access-Control-Allow-Origin' => '*',
28
+ 'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
29
+ 'Access-Control-Allow-Headers' => 'origin, x-csrftoken, content-type, accept'
30
+ }
31
+ if req.request_method == 'OPTIONS'
32
+ return [200 , headers, ""]
33
+ end
34
+
35
+ uuid = req.path_info.split('/')[-1]
36
+ debug "Checking for promise '#{uuid}'"
37
+
38
+ unless context = @@contexts[uuid]
39
+ return [404, {}, "Can't find requested promise"]
40
+ end
41
+ context[:timestamp] = Time.now # keep reaper away
42
+ promise = context[:promise]
43
+ case promise.status
44
+ when :pending
45
+ return retry_later(req, context)
46
+ when :rejected
47
+ body = {
48
+ type: 'error',
49
+ error: {
50
+ reason: promise.error_msg,
51
+ code: promise.error_code
52
+ }
53
+ }
54
+ headers['Content-Type'] = 'application/json'
55
+ return [500, headers, JSON.pretty_generate(body)]
56
+ end
57
+
58
+ # OK, the promise seems to have been resolved
59
+ content_type, body = promise.value
60
+ #puts ">>>>VALUE #{body.class} - #{content_type}>>>>> #{body}"
61
+ if body.is_a? Thin::AsyncResponse
62
+ return body.finish
63
+ end
64
+
65
+ begin
66
+ if content_type == 'text/html'
67
+ #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
68
+ body = convert_to_html(body, env, {}, context[:collection_set])
69
+ else
70
+ content_type = 'application/json'
71
+ body = JSON.pretty_generate(body)
72
+ end
73
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
74
+ proxy = OMF::SFA::Util::Promise.new
75
+ pex.promise.on_success do |d|
76
+ proxy.resolve [content_type, body]
77
+ end.on_error(proxy)
78
+ uuid = pex.uuid
79
+ path = "/promises/#{uuid}"
80
+ require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
81
+ self.class.register_promise(proxy, uuid, context[:html_reply], context[:collection_set])
82
+ debug "Redirecting to #{path}"
83
+ return [302, {'Location' => path}, ['Promised, but not ready yet.']]
84
+ end
85
+ headers['Content-Type'] = content_type
86
+ return [200 , headers, (body || '') + "\n"]
87
+
88
+ rescue RackException => rex
89
+ return rex.reply
90
+ rescue Exception => ex
91
+ body = {
92
+ type: 'error',
93
+ error: {
94
+ reason: ex.to_s,
95
+ bt: ex.backtrace #.select {|l| !l.start_with?('/') }
96
+ }
97
+ }
98
+ warn "ERROR: #{ex}"
99
+ debug ex.backtrace.join("\n")
100
+ headers['Content-Type'] = 'application/json'
101
+ return [500, headers, JSON.pretty_generate(body)]
102
+ end
103
+ end
104
+
105
+ def retry_later(req, context)
106
+ body = {
107
+ type: 'retry',
108
+ delay: 10, # rex.delay,
109
+ url: absolute_path(req.env['REQUEST_PATH']),
110
+ progress: context[:promise].progress.map do |ts, msg|
111
+ "#{ts.utc.iso8601}: #{msg}"
112
+ end
113
+ }
114
+ debug "Retry later request - #{req.url}"
115
+ headers = {'Content-Type' => 'application/json'}
116
+ return [504, headers, JSON.pretty_generate(body)]
117
+ end
118
+ end
119
+ end
@@ -8,6 +8,7 @@ require 'thin/async'
8
8
  require 'cgi'
9
9
 
10
10
  require 'omf_base/lobject'
11
+ require 'omf-sfa/util/promise'
11
12
 
12
13
 
13
14
  module OMF::SFA::AM::Rest
@@ -75,6 +76,12 @@ module OMF::SFA::AM::Rest
75
76
  end
76
77
  end
77
78
 
79
+ class TemporaryUnavailableException < RackException
80
+ def initialize()
81
+ super 504, "Upstream servers haven't responded yet, please try again a bit later"
82
+ end
83
+ end
84
+
78
85
  class RedirectException < Exception
79
86
  attr_reader :path
80
87
 
@@ -83,17 +90,68 @@ module OMF::SFA::AM::Rest
83
90
  end
84
91
  end
85
92
 
93
+ class ContentFoundException < Exception
94
+ def initialize(content, type = 'text', &block)
95
+ @type = case type.to_sym
96
+ when :json
97
+ 'application/json'
98
+ when :xml
99
+ 'text/xml'
100
+ when :text
101
+ 'text'
102
+ else
103
+ (type || 'text').to_s
104
+ end
105
+ if content.is_a? OMF::SFA::Util::Promise
106
+ @content = OMF::SFA::Util::Promise.new
107
+ content.on_success do |pv|
108
+ v = block ? block.call(pv) : pv
109
+ @content.resolve [@type, v]
110
+ end.on_error(@content).on_progress(@content)
111
+ else
112
+ @content = content
113
+ end
114
+ end
115
+
116
+ def reply
117
+ if @content.is_a? OMF::SFA::Util::Promise
118
+ require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
119
+ OMF::SFA::AM::Rest::PromiseHandler.register_promise(@content)
120
+ else
121
+ headers = {'Content-Type' => @type}
122
+ [200 , headers, @content]
123
+ end
124
+ end
125
+ end
126
+
127
+ # class PromiseUnresolvedException < Exception
128
+ # attr_reader :uuid, :promise
129
+ #
130
+ # def initialize(promise)
131
+ # @uuid = UUIDTools::UUID.random_create
132
+ # @promise = promise
133
+ # end
134
+ # end
135
+
86
136
  # Raised when a request triggers an async call whose
87
137
  # result we need before answering the request
88
138
  #
89
139
  class RetryLaterException < Exception
90
140
  attr_reader :delay
91
141
 
92
- def initialize(delay = 3)
142
+ def initialize(delay = 10)
93
143
  @delay = delay
94
144
  end
95
145
  end
96
146
 
147
+ # class FoundUnresolvedPromiseException < Exception
148
+ # attr_reader :promise
149
+ #
150
+ # def initialize(promise)
151
+ # @promise = promise
152
+ # end
153
+ # end
154
+
97
155
 
98
156
 
99
157
  class RestHandler < OMF::Base::LObject
@@ -147,40 +205,63 @@ module OMF::SFA::AM::Rest
147
205
  return [200 , headers, ""]
148
206
  end
149
207
  content_type, body = dispatch(req)
208
+ #puts "BODY(#{body.class}) - #{content_type}) >>>>> #{body}"
150
209
  if body.is_a? Thin::AsyncResponse
151
210
  return body.finish
152
211
  end
153
- if req['_format'] == 'html'
154
- #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
155
- body = convert_to_html(body, env, {}, Set.new((@coll_handlers || {}).keys))
156
- content_type = 'text/html'
157
- elsif content_type == 'application/json'
158
- body = JSON.pretty_generate(body)
159
- end
212
+ content_type, body = _format_body(body, content_type, req, env)
213
+ # if req['_format'] == 'html'
214
+ # #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
215
+ # body = convert_to_html(body, env, {}, Set.new((@coll_handlers || {}).keys))
216
+ # content_type = 'text/html'
217
+ # elsif content_type == 'application/json'
218
+ # body = JSON.pretty_generate(body)
219
+ # end
160
220
  #return [200 ,{'Content-Type' => content_type}, body + "\n"]
161
221
  headers['Content-Type'] = content_type
162
- return [200 , headers, body + "\n"]
222
+ return [200 , headers, (body || '') + "\n"]
223
+ rescue ContentFoundException => cex
224
+ return cex.reply
163
225
  rescue RackException => rex
226
+ unless rex.is_a? TemporaryUnavailableException
227
+ warn "Caught #{rex} - #{env['REQUEST_METHOD']} - #{env['REQUEST_PATH']}"
228
+ debug "#{rex.class} - #{req.inspect}"
229
+ debug rex.backtrace.join("\n")
230
+ end
164
231
  return rex.reply
165
- rescue RedirectException => rex
232
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
233
+ require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
234
+ OMF::SFA::AM::Rest::PromiseHandler.register_promise(pex.promise,
235
+ pex.uuid,
236
+ req['_format'] == 'html',
237
+ Set.new((@coll_handlers || {}).keys))
238
+ #return [302, {'Location' => path}, ['Promised, but not ready yet.']]
239
+ rescue OMF::SFA::AM::Rest::RedirectException => rex
166
240
  debug "Redirecting to #{rex.path}"
167
- return [301, {'Location' => rex.path, "Content-Type" => ""}, ['Next window!']]
241
+ return [302, {'Location' => rex.path}, ['Next window, please.']]
242
+
168
243
  # rescue OMF::SFA::AM::AMManagerException => aex
169
244
  # return RackException.new(400, aex.to_s).reply
170
- rescue RetryLaterException => rex
171
- body = {
172
- type: 'retry',
173
- delay: rex.delay
174
- }
175
- debug "Retry later request"
176
- if req['_format'] == 'html'
177
- headers['Refresh'] = rex.delay.to_s
178
- opts = {} #{html_header: "<META HTTP-EQUIV='refresh' CONTENT='#{rex.delay}'>"}
179
- body = convert_to_html(body, env, opts)
180
- return [200 , headers, body + "\n"]
181
- end
182
- headers['Content-Type'] = 'application/json'
183
- return [504, headers, JSON.pretty_generate(body)]
245
+ # rescue RetryLaterException => rex
246
+ # body = {
247
+ # type: 'retry',
248
+ # delay: rex.delay,
249
+ # request_id: Thread.current[:request_context_id] || 'unknown'
250
+ # }
251
+ # debug "Retry later request - #{req.url}"
252
+ # if req['_format'] == 'html'
253
+ # refresh = rex.delay.to_s
254
+ # if (req_id = Thread.current[:request_context_id])
255
+ # refresh += "; url=#{req.url}&_request_id=#{req_id}"
256
+ # end
257
+ # headers['Refresh'] = refresh # 10; url=
258
+ # headers['X-Request-ID'] = req_id
259
+ # opts = {} #{html_header: "<META HTTP-EQUIV='refresh' CONTENT='#{rex.delay}'>"}
260
+ # body = convert_to_html(body, env, opts)
261
+ # return [200 , headers, body + "\n"]
262
+ # end
263
+ # headers['Content-Type'] = 'application/json'
264
+ # return [504, headers, JSON.pretty_generate(body)]
184
265
 
185
266
  rescue Exception => ex
186
267
  body = {
@@ -197,6 +278,37 @@ module OMF::SFA::AM::Rest
197
278
  end
198
279
  end
199
280
 
281
+ def _format_body(body, content_type, req, env, proxy_promise = nil)
282
+ begin
283
+ if req['_format'] == 'html'
284
+ #body = self.class.convert_to_html(body, env, Set.new((@coll_handlers || {}).keys))
285
+ content_type = 'text/html'
286
+ body = convert_to_html(body, env, {}, Set.new((@coll_handlers || {}).keys))
287
+ elsif content_type == 'application/json'
288
+ body = JSON.pretty_generate(body)
289
+ end
290
+ [content_type, body]
291
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
292
+ proxy = OMF::SFA::Util::Promise.new
293
+ pex.promise.on_success do |d|
294
+ proxy.resolve [content_type, body]
295
+ end.on_error(proxy).on_progress(proxy)
296
+ raise OMF::SFA::Util::PromiseUnresolvedException.new proxy
297
+ end
298
+ end
299
+
300
+ # def _x(promise, req)
301
+ # uuid = 1 #pex.uuid
302
+ # path = "/promises/#{uuid}"
303
+ # require 'omf-sfa/am/am-rest/promise_handler' # delay loading as PromiseHandler sub classes this class
304
+ # OMF::SFA::AM::Rest::PromiseHandler.register_promise(promise,
305
+ # uuid,
306
+ # req['_format'] == 'html',
307
+ # Set.new((@coll_handlers || {}).keys))
308
+ # debug "Redirecting to #{path}"
309
+ # return [302, {'Location' => path}, ['Promised, but not ready yet.']]
310
+ # end
311
+
200
312
  def on_get(resource_uri, opts)
201
313
  debug 'get: resource_uri: "', resource_uri, '"'
202
314
  if resource_uri
@@ -207,6 +319,11 @@ module OMF::SFA::AM::Rest
207
319
  end
208
320
  end
209
321
 
322
+ def on_put(resource_uri, opts)
323
+ debug '>>> PUT NOT IMPLEMENTED'
324
+ raise UnsupportedMethodException.new('on_put', @resource_class)
325
+ end
326
+
210
327
  def on_post(resource_uri, opts)
211
328
  #debug 'POST: resource_uri "', resource_uri, '" - ', opts.inspect
212
329
  description, format = parse_body(opts, [:json, :form])
@@ -214,7 +331,7 @@ module OMF::SFA::AM::Rest
214
331
 
215
332
  if resource = opts[:resource]
216
333
  debug 'POST: Modify ', resource, ' --- ', resource.class
217
- modify_resource(resource, description, opts)
334
+ resource = modify_resource(resource, description, opts)
218
335
  else
219
336
  if description.is_a? Array
220
337
  resources = description.map do |d|
@@ -268,7 +385,7 @@ module OMF::SFA::AM::Rest
268
385
 
269
386
  def find_handler(path, opts)
270
387
  #debug "find_handler: path; '#{path}' opts: #{opts}"
271
- debug "find_handler: path; '#{path}'"
388
+ debug "find_handler: path: '#{path}'"
272
389
  rid = path.shift
273
390
  resource_id = opts[:resource_uri] = (rid ? URI.decode(rid) : nil) # make sure we get rid of any URI encoding
274
391
  opts[:resource] = nil
@@ -279,6 +396,7 @@ module OMF::SFA::AM::Rest
279
396
 
280
397
  raise OMF::SFA::AM::Rest::UnknownResourceException.new "Unknown resource '#{resource_id}'." unless resource
281
398
  opts[:context] = resource
399
+ opts[:contexts][opts[:context_name].to_sym] = resource
282
400
  comp = path.shift
283
401
  if (handler = @coll_handlers[comp.to_sym])
284
402
  opts[:context_name] = comp
@@ -292,7 +410,6 @@ module OMF::SFA::AM::Rest
292
410
  end
293
411
 
294
412
 
295
-
296
413
  protected
297
414
 
298
415
  def modify_resource(resource, description, opts)
@@ -301,6 +418,7 @@ module OMF::SFA::AM::Rest
301
418
  end
302
419
  description.delete(:href)
303
420
  resource.update(description) ? resource : nil
421
+ resource
304
422
  end
305
423
 
306
424
 
@@ -384,15 +502,34 @@ module OMF::SFA::AM::Rest
384
502
  #
385
503
  def populate_opts(req, opts)
386
504
  opts[:req] = req
505
+ opts[:context_name] = (req.env['REQUEST_PATH'].split('/') - req.path_info.split('/'))[-1]
506
+ opts[:contexts] ||= {}
387
507
  path = req.path_info.split('/').select { |p| !p.empty? }
388
508
  opts[:target] = find_handler(path, opts)
389
509
  rl = req.params.delete('_level')
390
- opts[:max_level] = rl ? rl.to_i : 0
510
+ opts[:max_level] = rl ? rl.to_i : 1
391
511
  #opts[:target].inspect
392
512
  opts
393
513
  end
394
514
 
515
+ # Return a named context resource. If it is a promise and not yet
516
+ # available, a TemporaryUnavailableException is being thrown.
517
+ #
518
+ # For instance if we have a path /users/xxx/friends/yyy/favorites
519
+ # by the time we get to the 'favorites' handler, we can access
520
+ # the respective 'users', 'friends' object through this method.
521
+ # PLEASE note the plurals 'users', 'friends'.
522
+ #
523
+ def get_context_resource(name, opts)
524
+ resource = opts[:contexts][name]
525
+ if resource.is_a? OMF::SFA::Util::Promise
526
+ resource = resource.value(OMF::SFA::AM::Rest::TemporaryUnavailableException)
527
+ end
528
+ resource
529
+ end
530
+
395
531
  def parse_body(opts, allowed_formats = [:json, :xml])
532
+ puts "ALLOWD>>>> #{allowed_formats}"
396
533
  req = opts[:req]
397
534
  body = req.body #req.POST
398
535
  raise EmptyBodyException.new unless body
@@ -424,12 +561,16 @@ module OMF::SFA::AM::Rest
424
561
  raise UnsupportedBodyFormatException.new(:json) unless allowed_formats.include?(:json)
425
562
  jb = JSON.parse(body)
426
563
  return [_rec_sym_keys(jb), :json]
564
+ when 'application/gjson'
565
+ raise UnsupportedBodyFormatException.new(:gjson) unless allowed_formats.include?(:gjson)
566
+ jb = JSON.parse(body)
567
+ return [_rec_sym_keys(jb), :gjson]
427
568
  when 'text/xml'
428
569
  xb = Nokogiri::XML(body)
429
570
  raise UnsupportedBodyFormatException.new(:xml) unless allowed_formats.include?(:xml)
430
571
  return [xb, :xml]
431
572
  when 'application/x-www-form-urlencoded'
432
- raise UnsupportedBodyFormatException.new(:xml) unless allowed_formats.include?(:form)
573
+ raise UnsupportedBodyFormatException.new(:form) unless allowed_formats.include?(:form)
433
574
  fb = req.POST
434
575
  #puts "FORM: #{fb.inspect}"
435
576
  return [fb, :form]
@@ -447,11 +588,18 @@ module OMF::SFA::AM::Rest
447
588
  def dispatch(req)
448
589
  opts = {}
449
590
  populate_opts(req, opts)
450
- opts[:req] = req
591
+ #opts[:req] = req
451
592
  #puts "OPTS>>>> #{opts.inspect}"
452
593
  method = req.request_method
453
594
  target = opts[:target] #|| self
595
+ if target.is_a? OMF::SFA::AM::Rest::ContentFoundException
596
+ raise target
597
+ end
454
598
  resource_uri = opts[:resource_uri]
599
+ _dispatch(method, target, resource_uri, opts)
600
+ end
601
+
602
+ def _dispatch(method, target, resource_uri, opts)
455
603
  case method
456
604
  when 'GET'
457
605
  res = target.on_get(resource_uri, opts)
@@ -468,22 +616,30 @@ module OMF::SFA::AM::Rest
468
616
 
469
617
  def show_resource_status(resource, opts)
470
618
  if resource
619
+ resource = resolve_promise(resource) do |r|
620
+ show_resource_status(r, opts)
621
+ end
471
622
  about = opts[:req].path
472
- props = resource.to_hash({}, :max_level => opts[:max_level])
623
+ refresh = ['', '1', 't', 'T', 'true', 'TRUE'].include?(opts[:req].params['_refresh'])
624
+ props = resource.to_hash({}, max_level: opts[:max_level], refresh: refresh)
473
625
  props.delete(:type)
474
- res = {
626
+ res = after_resource_to_hash_hook({
475
627
  #:about => about,
476
628
  :type => resource.resource_type,
477
- }.merge!(props)
629
+ }.merge!(props), resource)
478
630
  else
479
631
  res = {:error => 'Unknown resource'}
480
632
  end
481
-
482
- ['application/json', res]
633
+ check_for_promises 'application/json', res
483
634
  end
484
635
 
636
+ def after_resource_to_hash_hook(res_hash, resource)
637
+ res_hash
638
+ end
485
639
 
486
-
640
+ def absolute_path(rel_path)
641
+ "http://#{Thread.current[:http_host]}#{rel_path.start_with?('/') ? '' : '/'}#{rel_path}"
642
+ end
487
643
 
488
644
  def find_resource(resource_uri, description = {}, opts = {})
489
645
  descr = description.dup
@@ -497,10 +653,21 @@ module OMF::SFA::AM::Rest
497
653
  end
498
654
 
499
655
  #authenticator = Thread.current["authenticator"]
656
+ descr = _find_resource_before_hook(descr, opts)
657
+ _find_resource(descr)
658
+ end
659
+
660
+ # Allow sub class to override actual finding of resource - may create it on the fly
661
+ def _find_resource(descr)
500
662
  debug "Finding #{@resource_class}.first(#{descr})"
501
663
  @resource_class.first(descr)
502
664
  end
503
665
 
666
+ # Allow sub class to override search criteria for resource
667
+ def _find_resource_before_hook(descr, opts)
668
+ descr
669
+ end
670
+
504
671
  def show_resource_list(opts)
505
672
  # authenticator = Thread.current["authenticator"]
506
673
  if (context = opts[:context])
@@ -512,12 +679,21 @@ module OMF::SFA::AM::Rest
512
679
  end
513
680
 
514
681
  def show_resources(resources, resource_name, opts)
682
+ resources = resolve_promise(resources) do |r|
683
+ show_resources(r, resource_name, opts)
684
+ end
515
685
  #hopts = {max_level: opts[:max_level], level: 1}
516
- hopts = {max_level: opts[:max_level], level: 0}
686
+ hopts = {max_level: opts[:max_level] - 1, level: 0}
517
687
  objs = {}
518
688
  res_hash = resources.map do |a|
689
+ a = resolve_promise(a) do |r|
690
+ # The resolved promise was embeded, so we need to start afresh from the top
691
+ show_resources(resources, resource_name, opts)
692
+ end
519
693
  next unless a # TODO: This seems to be a bug in OProperty (removing objects)
520
- a.to_hash(objs, hopts)
694
+ #puts ">>>>>>>>> #{a.to_hash({}, hopts.dup)}"
695
+ #a.to_hash(objs, hopts.dup)
696
+ after_resource_to_hash_hook(a.to_hash(objs, hopts), a)
521
697
  #a.to_hash_brief(:href_use_class_prefix => true)
522
698
  end.compact
523
699
  if resource_name
@@ -529,9 +705,84 @@ module OMF::SFA::AM::Rest
529
705
  else
530
706
  res = res_hash
531
707
  end
532
- ['application/json', res]
708
+ check_for_promises 'application/json', res
533
709
  end
534
710
 
711
+ # Check if 'value' is a promise. If not, return it immediately. If it is
712
+ # a promise, return it's value if it has been already resolved. Otherwise
713
+ # throw a PromiseDeferException. If the promise gets resolved at some later
714
+ # stage the 'block' is called and is expected to return the same result as
715
+ # the caller of this function would have returned if this function would have
716
+ # returned immediately.
717
+ #
718
+ def resolve_promise(value, &block)
719
+ if (promise = value).is_a? OMF::SFA::Util::Promise
720
+ case promise.status
721
+ when :pending
722
+ proxy = OMF::SFA::Util::Promise.new
723
+ promise.on_success do |d|
724
+ proxy.resolve block.call(d)
725
+ end.on_error(proxy).on_progress(proxy)
726
+ raise OMF::SFA::Util::PromiseUnresolvedException.new proxy
727
+ when :rejected
728
+ raise promise.error_msg
729
+ else
730
+ value = promise.value
731
+ end
732
+ end
733
+ #puts "RESOLVE PROMISE(#{value.class}>>> #{value}"
734
+ value
735
+ end
736
+
737
+ # Check if any elements in res, which is either an array
738
+ # or hash, is a promise. If one is found, and can't be resolved,
739
+ # a PromiseDeferException is thrown. The proxy promise in the exception
740
+ # will monitor the unresolved promises and after all of them are resolved
741
+ # will resolve the associated promise with a 'clean' res.
742
+ #
743
+ def check_for_promises(mime_type, res, proxy = nil)
744
+ begin
745
+ res = _scan_for_promises(res)
746
+ rescue OMF::SFA::Util::PromiseUnresolvedException => pex
747
+ unless proxy
748
+ proxy = OMF::SFA::Util::Promise.new
749
+ pex.promise.on_success do |x|
750
+ check_for_promises(mime_type, res, proxy)
751
+ end.on_error do |ec, em|
752
+ proxy.reject(ec, em)
753
+ end.on_progress(proxy)
754
+ raise OMF::SFA::Util::PromiseUnresolvedException.new proxy
755
+ end
756
+ end
757
+ if proxy
758
+ proxy.resolve [mime_type, res]
759
+ end
760
+ [mime_type, res]
761
+ end
762
+
763
+ def _scan_for_promises(res)
764
+ if res.is_a? Array
765
+ res = res.map do |el|
766
+ _scan_for_promises(el)
767
+ end
768
+ elsif res.is_a? Hash
769
+ res.each do |key, val|
770
+ res[key] = _scan_for_promises(val)
771
+ end
772
+ elsif res.is_a? OMF::SFA::Util::Promise
773
+ case res.status
774
+ when :resolved
775
+ return res.value
776
+ when :rejected
777
+ raise res.err_message
778
+ else
779
+ raise OMF::SFA::Util::PromiseUnresolvedException.new(res)
780
+ end
781
+ end
782
+ res # seems to be 'normal' value
783
+ end
784
+
785
+
535
786
  def show_deleted_resource(uuid)
536
787
  res = {
537
788
  uuid: uuid,
@@ -602,12 +853,13 @@ module OMF::SFA::AM::Rest
602
853
  tmpl
603
854
  end
604
855
 
605
- def convert_to_html(body, env, opts, collections = Set.new)
856
+ def convert_to_html(obj, env, opts, collections = Set.new)
606
857
  req = ::Rack::Request.new(env)
607
858
  opts = {
608
859
  collections: collections,
609
860
  level: 0,
610
- href_prefix: "#{req.path}/"
861
+ href_prefix: "#{req.path}/",
862
+ env: env
611
863
  }.merge(opts)
612
864
 
613
865
  path = req.path.split('/').select { |p| !p.empty? }
@@ -617,11 +869,11 @@ module OMF::SFA::AM::Rest
617
869
  end
618
870
 
619
871
  res = []
620
- _convert_obj_to_html(body, nil, res, opts)
872
+ _convert_obj_to_html(obj, nil, res, opts)
621
873
 
622
874
  render_html(
623
875
  header: opts[:html_header] || '',
624
- result: body,
876
+ result: obj,
625
877
  title: @@service_name || env["HTTP_HOST"],
626
878
  service: h2.join('/'),
627
879
  content: res.join("\n")
@@ -636,6 +888,9 @@ module OMF::SFA::AM::Rest
636
888
  def _convert_obj_to_html(obj, ref_name, res, opts)
637
889
  klass = obj.class
638
890
  #puts "CONVERT>>>> #{ref_name} ... #{obj.class}::#{obj.to_s[0 .. 80]}"
891
+ if obj.is_a? OMF::SFA::Util::Promise
892
+ obj = obj.to_html()
893
+ end
639
894
  if (obj.is_a? OMF::SFA::Resource::OPropertyArray) || obj.is_a?(Array)
640
895
  if obj.empty?
641
896
  res << '<span class="empty">empty</span>'
@@ -661,7 +916,7 @@ module OMF::SFA::AM::Rest
661
916
  end
662
917
 
663
918
  def _convert_array_to_html(array, ref_name, res, opts)
664
- opts = opts.merge(level: opts[:level] + 1)
919
+ opts = opts.merge(level: opts[:level] + 1, context: array)
665
920
  array.each do |obj|
666
921
  #puts "AAA>>>> #{obj}::#{opts}"
667
922
  name = nil
@@ -684,6 +939,7 @@ module OMF::SFA::AM::Rest
684
939
 
685
940
  def _convert_hash_to_html(hash, ref_name, res, opts)
686
941
  #puts ">>>> #{hash}::#{opts}"
942
+ opts = opts.merge(context: hash)
687
943
  hash.each do |key, obj|
688
944
  #key = "#{key}-#{opts[:level]}-#{opts[:collections].to_a.inspect}"
689
945
  if opts[:level] == 0 && opts[:collections].include?(key.to_sym)