MuranoCLI 3.0.1 → 3.0.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.agignore +1 -0
  3. data/.rubocop.yml +67 -5
  4. data/Gemfile +6 -3
  5. data/MuranoCLI.gemspec +14 -10
  6. data/README.markdown +299 -126
  7. data/Rakefile +6 -1
  8. data/bin/murano +2 -2
  9. data/docs/completions/murano_completion-bash +93 -0
  10. data/lib/MrMurano.rb +19 -2
  11. data/lib/MrMurano/Business.rb +22 -19
  12. data/lib/MrMurano/Config.rb +19 -9
  13. data/lib/MrMurano/Content.rb +4 -4
  14. data/lib/MrMurano/Exchange-Element.rb +99 -0
  15. data/lib/MrMurano/Exchange.rb +137 -0
  16. data/lib/MrMurano/Gateway.rb +9 -9
  17. data/lib/MrMurano/Keystore.rb +4 -2
  18. data/lib/MrMurano/ReCommander.rb +3 -5
  19. data/lib/MrMurano/Solution-ServiceConfig.rb +12 -12
  20. data/lib/MrMurano/Solution-Services.rb +15 -14
  21. data/lib/MrMurano/Solution-Users.rb +2 -2
  22. data/lib/MrMurano/Solution.rb +43 -49
  23. data/lib/MrMurano/SolutionId.rb +28 -28
  24. data/lib/MrMurano/SyncUpDown.rb +32 -22
  25. data/lib/MrMurano/Webservice-Endpoint.rb +2 -1
  26. data/lib/MrMurano/Webservice.rb +5 -5
  27. data/lib/MrMurano/commands.rb +2 -1
  28. data/lib/MrMurano/commands/business.rb +21 -19
  29. data/lib/MrMurano/commands/domain.rb +16 -2
  30. data/lib/MrMurano/commands/exchange.rb +272 -0
  31. data/lib/MrMurano/commands/globals.rb +17 -1
  32. data/lib/MrMurano/commands/init.rb +3 -3
  33. data/lib/MrMurano/commands/link.rb +16 -16
  34. data/lib/MrMurano/commands/postgresql.rb +2 -2
  35. data/lib/MrMurano/commands/show.rb +13 -7
  36. data/lib/MrMurano/commands/solution.rb +23 -17
  37. data/lib/MrMurano/commands/solution_picker.rb +49 -44
  38. data/lib/MrMurano/commands/sync.rb +2 -1
  39. data/lib/MrMurano/commands/timeseries.rb +2 -2
  40. data/lib/MrMurano/commands/tsdb.rb +2 -2
  41. data/lib/MrMurano/hash.rb +19 -7
  42. data/lib/MrMurano/http.rb +12 -2
  43. data/lib/MrMurano/orderedhash.rb +200 -0
  44. data/lib/MrMurano/spec_commander.rb +98 -0
  45. data/lib/MrMurano/verbosing.rb +2 -2
  46. data/lib/MrMurano/version.rb +2 -2
  47. data/spec/Business_spec.rb +8 -6
  48. data/spec/Solution-ServiceConfig_spec.rb +1 -1
  49. data/spec/SyncUpDown_spec.rb +6 -6
  50. data/spec/_workspace.rb +9 -4
  51. data/spec/cmd_business_spec.rb +8 -2
  52. data/spec/cmd_common.rb +266 -25
  53. data/spec/cmd_exchange_spec.rb +118 -0
  54. data/spec/cmd_help_spec.rb +54 -13
  55. data/spec/cmd_init_spec.rb +1 -12
  56. data/spec/cmd_link_spec.rb +94 -72
  57. data/spec/spec_helper.rb +11 -16
  58. metadata +23 -17
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.23 /coding: utf-8
1
+ # Last Modified: 2017.09.11 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -9,68 +9,68 @@ require 'MrMurano/verbosing'
9
9
 
10
10
  module MrMurano
11
11
  module SolutionId
12
- INVALID_SID = '-1'
12
+ INVALID_API_ID = '-1'
13
13
  UNEXPECTED_TYPE_OR_ERROR_MSG = (
14
14
  'Unexpected result type or error: assuming empty instead'
15
15
  )
16
16
 
17
+ attr_reader :api_id
18
+ attr_reader :valid_api_id
17
19
  attr_reader :sid
18
- attr_reader :valid_sid
19
20
 
20
- def init_sid!(sid=nil)
21
- @valid_sid = false
21
+ def init_api_id!(api_id=nil)
22
+ @valid_api_id = false
22
23
  unless defined?(@solntype) && @solntype
23
24
  # Note that 'solution.id' isn't an actual config setting;
24
25
  # see instead 'application.id' and 'product.id'. We just
25
26
  # use 'solution.id' to indicate that the caller specified
26
27
  # a solution ID explicitly (i.e., it's not from the $cfg).
27
- raise 'Missing sid or class @solntype!?' if sid.to_s.empty?
28
+ raise 'Missing api_id or class @solntype!?' if api_id.to_s.empty?
28
29
  @solntype = 'solution.id'
29
30
  end
30
- if sid
31
- self.sid = sid
31
+ if api_id
32
+ self.api_id = api_id
32
33
  else
33
34
  # Get the application.id or product.id.
34
- self.sid = $cfg[@solntype]
35
+ self.api_id = $cfg[@solntype]
35
36
  end
36
37
  # Maybe raise 'No application!' or 'No product!'.
37
- return unless @sid.to_s.empty?
38
+ return unless @api_id.to_s.empty?
38
39
  raise MrMurano::ConfigError.new("No #{/(.*).id/.match(@solntype)[1]} ID!")
39
40
  end
40
41
 
41
- def sid?
42
- # The @sid should never be nil or empty, but let's at least check.
43
- @sid != INVALID_SID && !@sid.to_s.empty?
42
+ def api_id?
43
+ # The @api_id should never be nil or empty, but let's at least check.
44
+ @api_id != INVALID_API_ID && !@api_id.to_s.empty?
44
45
  end
45
46
 
46
- def sid=(sid)
47
- sid = INVALID_SID if sid.nil? || !sid.is_a?(String) || sid.empty?
48
- if sid.to_s.empty? || sid == INVALID_SID || (defined?(@sid) && sid != @sid)
49
- @valid_sid = false
47
+ def api_id=(api_id)
48
+ api_id = INVALID_API_ID if api_id.nil? || !api_id.is_a?(String) || api_id.empty?
49
+ if api_id.to_s.empty? || api_id == INVALID_API_ID || (defined?(@api_id) && api_id != @api_id)
50
+ @valid_api_id = false
50
51
  end
51
- @sid = sid
52
- # MAGIC_NUMBER: The 2nd element is the solution ID, e.g., solution/<sid>/...
53
- raise "Unexpected @uriparts_sidex #{@uriparts_sidex}" unless @uriparts_sidex == 1
52
+ @api_id = api_id
53
+ # MAGIC_NUMBER: The 2nd element is the solution ID, e.g., solution/<api_id>/...
54
+ raise "Unexpected @uriparts_apidex #{@uriparts_apidex}" unless @uriparts_apidex == 1
54
55
  # We're called on initialize before @uriparts is built, so don't always do this.
55
- @uriparts[@uriparts_sidex] = @sid if defined?(@uriparts)
56
+ @uriparts[@uriparts_apidex] = @api_id if defined?(@uriparts)
56
57
  end
57
58
 
58
59
  def affirm_valid
59
- @valid_sid = true
60
+ @valid_api_id = true
60
61
  end
61
62
 
62
- def valid_sid?
63
- @valid_sid
63
+ def valid_api_id?
64
+ @valid_api_id
64
65
  end
65
66
 
66
- # rubocop:disable Style/MethodName
67
- def apiId
68
- @sid
67
+ def api_id
68
+ @api_id
69
69
  end
70
70
 
71
71
  def endpoint(_path='')
72
72
  # This is hopefully just a DEV error, and not something user will ever see!
73
- return unless @uriparts[@uriparts_sidex] == INVALID_SID
73
+ return unless @uriparts[@uriparts_apidex] == INVALID_API_ID
74
74
  error("Solution ID missing! Invalid #{MrMurano::Verbose.fancy_ticks(@solntype)}")
75
75
  exit 2
76
76
  end
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.23 /coding: utf-8
1
+ # Last Modified: 2017.09.11 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -295,17 +295,26 @@ module MrMurano
295
295
  #
296
296
  # @param local [Pathname] Full path of where to download to
297
297
  # @param item [Item] The item to download
298
- def download(local, item)
298
+ def download(local, item, options={})
299
299
  #if item[:bundled]
300
300
  # warning "Not downloading into bundled item #{synckey(item)}"
301
301
  # return
302
302
  #end
303
303
  id = item[@itemkey.to_sym]
304
- if id.nil?
305
- debug "!!! Missing '#{@itemkey}', using :id instead!"
306
- debug ":id => #{item[:id]}"
307
- id = item[:id]
308
- raise "Both #{@itemkey} and id in item are nil!" if id.nil?
304
+ if id.to_s.empty?
305
+ # 2017-09-05: MRMUR-156: User seeing this.
306
+ if @itemkey.to_sym != :id
307
+ debug "!!! Missing '#{@itemkey}', trying :id instead"
308
+ id = item[:id]
309
+ end
310
+ if id.to_s.empty?
311
+ debug %(Remote item "#{item[:name]}" missing :id / local: #{local} / item: #{item})
312
+ return if options[:ignore_errors]
313
+ error %(Remote item missing :id => #{local})
314
+ print %(You can ignore this error using --ignore-errors)
315
+ exit 1
316
+ end
317
+ debug ":id => #{id}"
309
318
  end
310
319
 
311
320
  relpath = local.relative_path_from(Pathname.pwd).to_s
@@ -400,14 +409,14 @@ module MrMurano
400
409
  end
401
410
 
402
411
  def syncup_before
403
- syncable_validate_sid
412
+ syncable_validate_api_id
404
413
  end
405
414
 
406
415
  def syncup_after
407
416
  end
408
417
 
409
418
  def syncdown_before
410
- syncable_validate_sid
419
+ syncable_validate_api_id
411
420
  end
412
421
 
413
422
  def syncdown_after(_local)
@@ -738,13 +747,13 @@ module MrMurano
738
747
  end
739
748
  toadd.each do |item|
740
749
  syncdown_item(item, into, options, :create, 'Adding') do |dest, aitem|
741
- download(dest, aitem)
750
+ download(dest, aitem, options)
742
751
  num_synced += 1
743
752
  end
744
753
  end
745
754
  tomod.each do |item|
746
755
  syncdown_item(item, into, options, :update, 'Updating') do |dest, aitem|
747
- download(dest, aitem)
756
+ download(dest, aitem, options)
748
757
  num_synced += 1
749
758
  end
750
759
  end
@@ -908,9 +917,10 @@ module MrMurano
908
917
  # Get the solution name from the config.
909
918
  # Convert, e.g., application.id => application.name
910
919
  soln_name = $cfg[@solntype.gsub(/(.*)\.id/, '\1.name')]
911
- # Skip this syncable if the sid is not set, or if user wants to skip by solution.
920
+ # Skip this syncable if the api_id is not set, or if user wants to skip
921
+ # by solution.
912
922
  skip_sol = false
913
- if !sid? ||
923
+ if !api_id? ||
914
924
  (options[:type] == :application && @solntype != 'application.id') ||
915
925
  (options[:type] == :product && @solntype != 'product.id')
916
926
  skip_sol = true
@@ -918,17 +928,17 @@ module MrMurano
918
928
  tested = false
919
929
  passed = false
920
930
  if @solntype == 'application.id'
921
- # elevate_hash magically makes the hash return false rather than nil
922
- # on unknown keys, so preface with a key? guard.
931
+ # elevate_hash makes the hash return false rather than
932
+ # nil on unknown keys, so preface with a key? guard.
923
933
  if options.key?(:application) && !options[:application].to_s.empty?
924
934
  if soln_name =~ /#{Regexp.escape(options[:application])}/i ||
925
- sid =~ /#{Regexp.escape(options[:application])}/i
935
+ api_id =~ /#{Regexp.escape(options[:application])}/i
926
936
  passed = true
927
937
  end
928
938
  tested = true
929
939
  end
930
940
  if options.key?(:application_id) && !options[:application_id].to_s.empty?
931
- passed = true if options[:application_id] == sid
941
+ passed = true if options[:application_id] == api_id
932
942
  tested = true
933
943
  end
934
944
  if options.key?(:application_name) && !options[:application_name].to_s.empty?
@@ -938,13 +948,13 @@ module MrMurano
938
948
  elsif @solntype == 'product.id'
939
949
  if options.key?(:product) && !options[:product].to_s.empty?
940
950
  if soln_name =~ /#{Regexp.escape(options[:product])}/i ||
941
- sid =~ /#{Regexp.escape(options[:product])}/i
951
+ api_id =~ /#{Regexp.escape(options[:product])}/i
942
952
  passed = true
943
953
  end
944
954
  tested = true
945
955
  end
946
956
  if options.key?(:product_id) && !options[:product_id].to_s.empty?
947
- passed = true if options[:product_id] == sid
957
+ passed = true if options[:product_id] == api_id
948
958
  tested = true
949
959
  end
950
960
  if options.key?(:product_name) && !options[:product_name].to_s.empty?
@@ -960,13 +970,13 @@ module MrMurano
960
970
  ret
961
971
  end
962
972
 
963
- def syncable_validate_sid
973
+ def syncable_validate_api_id
964
974
  # 2017-07-02: Now that there are multiple solution types, and because
965
975
  # SyncRoot.add is called on different classes that go with either or
966
976
  # both products and applications, if a user only created one solution,
967
- # then some syncables will have their sid set to -1, because there's
977
+ # then some syncables will have their api_id set to -1, because there's
968
978
  # not a corresponding solution in Murano.
969
- raise 'Syncable missing sid or not valid_sid??!' unless sid?
979
+ raise 'Syncable missing api_id or not valid_api_id??!' unless api_id?
970
980
  end
971
981
 
972
982
  def items_lists(options, selected)
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.22 /coding: utf-8
1
+ # Last Modified: 2017.09.07 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -108,6 +108,7 @@ module MrMurano
108
108
  # @param modify [Boolean] True if item exists already and this is changing it
109
109
  def upload(local, remote, _modify)
110
110
  local = Pathname.new(local) unless local.is_a? Pathname
111
+ # MAYBE/2017-09-07: Honor options.ignore_errors here?
111
112
  raise 'no file' unless local.exist?
112
113
  # we assume these are small enough to slurp.
113
114
  if remote.script.nil?
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.16 /coding: utf-8
1
+ # Last Modified: 2017.09.11 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -22,11 +22,11 @@ module MrMurano
22
22
 
23
23
  def initialize
24
24
  @solntype = 'application.id'
25
- @uriparts_sidex = 1
26
- init_sid!
25
+ @uriparts_apidex = 1
26
+ init_api_id!
27
27
  # FIXME/2017-06-05/MRMUR-XXXX: Update to new endpoint.
28
- #@uriparts = [:service, @sid, :webservice]
29
- @uriparts = [:solution, @sid]
28
+ #@uriparts = [:service, @api_id, :webservice]
29
+ @uriparts = [:solution, @api_id]
30
30
  @itemkey = :id
31
31
  #@locationbase = $cfg['location.base']
32
32
  @location = nil
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.16 /coding: utf-8
1
+ # Last Modified: 2017.08.29 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -17,6 +17,7 @@ require 'MrMurano/commands/content'
17
17
  require 'MrMurano/commands/cors'
18
18
  require 'MrMurano/commands/devices'
19
19
  require 'MrMurano/commands/domain'
20
+ require 'MrMurano/commands/exchange'
20
21
  require 'MrMurano/commands/globals'
21
22
  require 'MrMurano/commands/keystore'
22
23
  require 'MrMurano/commands/init'
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.23 /coding: utf-8
1
+ # Last Modified: 2017.09.11 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -12,8 +12,8 @@ require 'MrMurano/ReCommander'
12
12
 
13
13
  MSG_BUSINESSES_NONE_FOUND = 'No businesses found' unless defined? MSG_BUSINESSES_NONE_FOUND
14
14
 
15
- # *** Base business command help.
16
- # -------------------------------
15
+ # *** Base business command help
16
+ # ------------------------------
17
17
 
18
18
  command :business do |c|
19
19
  c.syntax = %(murano business)
@@ -31,16 +31,14 @@ Commands for working with businesses.
31
31
  end
32
32
  alias_command 'businesses', 'business'
33
33
 
34
- # *** Common business command options.
35
- # ------------------------------------
34
+ # *** Common business command options
35
+ # -----------------------------------
36
36
 
37
- def cmd_business_add_options(c)
37
+ def cmd_table_output_add_options(c)
38
38
  # MAYBE/2017-08-15: Rename to --id-only.
39
39
  c.option '--idonly', 'Only return the IDs'
40
- c.option '--[no-]brief', 'Show fewer fields: only Business ID and name'
41
- # LATER/2017-08-17: Move -o to a file with --json, etc. (make
42
- # easier to use --json/--yaml/--output any file, without
43
- # having to use obscure `-c tool.outformat=json`).
40
+ c.option '--[no-]brief', 'Show fewer fields: show only IDs and names'
41
+ # MAYBE/2017-08-17: Move -o option to globals.rb and apply to all commands.
44
42
  c.option '-o', '--output FILE', 'Download to file instead of STDOUT'
45
43
  end
46
44
 
@@ -83,8 +81,8 @@ def any_business_pickers?(options)
83
81
  num_ways > 0
84
82
  end
85
83
 
86
- # *** Business commands: list and find.
87
- # -------------------------------------
84
+ # *** Business commands: list and find
85
+ # ------------------------------------
88
86
 
89
87
  command 'business list' do |c|
90
88
  c.syntax = %(murano business list [--options])
@@ -94,7 +92,7 @@ List businesses.
94
92
  ).strip
95
93
  c.project_not_required = true
96
94
 
97
- cmd_business_add_options(c)
95
+ cmd_table_output_add_options(c)
98
96
 
99
97
  c.action do |args, options|
100
98
  c.verify_arg_count!(args)
@@ -111,7 +109,7 @@ Find business by name or ID.
111
109
  ).strip
112
110
  c.project_not_required = true
113
111
 
114
- cmd_business_add_options(c)
112
+ cmd_table_output_add_options(c)
115
113
 
116
114
  # Add --business/-id/-name options.
117
115
  cmd_option_business_pickers(c)
@@ -130,8 +128,8 @@ Find business by name or ID.
130
128
  end
131
129
  end
132
130
 
133
- # *** Business actions helpers.
134
- # -----------------------------
131
+ # *** Business actions helpers
132
+ # ----------------------------
135
133
 
136
134
  def business_find_or_ask!(acc, options)
137
135
  #any_business_pickers?(options)
@@ -263,9 +261,9 @@ def cmd_business_find_businesses(acc, args, options)
263
261
  bizz
264
262
  end
265
263
 
266
- def cmd_business_output_businesses(acc, bizz, options)
264
+ def cmd_business_header_and_bizz(bizz, options)
267
265
  if options.idonly
268
- headers = [:bizid]
266
+ headers = %i[bizid]
269
267
  bizz = bizz.map(&:bizid)
270
268
  elsif options.brief
271
269
  #headers = %i[bizid role name]
@@ -274,7 +272,7 @@ def cmd_business_output_businesses(acc, bizz, options)
274
272
  bizz = bizz.map { |biz| [biz.bizid, biz.name] }
275
273
  else
276
274
  # 2017-08-16: There are only 3 keys: bizid, role, and name.
277
- headers = bizz[0].meta.keys
275
+ headers = (bizz[0] && bizz[0].meta.keys) || []
278
276
  headers.sort_by! do |hdr|
279
277
  case hdr
280
278
  when :bizid
@@ -289,7 +287,11 @@ def cmd_business_output_businesses(acc, bizz, options)
289
287
  end
290
288
  bizz = bizz.map { |biz| headers.map { |key| biz.meta[key] } }
291
289
  end
290
+ [headers, bizz]
291
+ end
292
292
 
293
+ def cmd_business_output_businesses(acc, bizz, options)
294
+ headers, bizz = cmd_business_header_and_bizz(bizz, options)
293
295
  io = File.open(options.output, 'w') if options.output
294
296
  acc.outf(bizz, io) do |dd, ios|
295
297
  if options.idonly
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.16 /coding: utf-8
1
+ # Last Modified: 2017.09.11 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -41,7 +41,21 @@ Print the domain for this solution.
41
41
 
42
42
  solz.each do |sol|
43
43
  if !options.brief
44
- say(sol.pretty_desc(add_type: true, raw_url: options.raw))
44
+ if $cfg['tool.outformat'] == 'best'
45
+ say(sol.pretty_desc(add_type: true, raw_url: options.raw))
46
+ else
47
+ dobj = {}
48
+ dobj[:type] = sol.type.to_s.capitalize
49
+ dobj[:name] = sol.name || ''
50
+ dobj[:api_id] = sol.api_id || ''
51
+ dobj[:sid] = sol.sid || ''
52
+ dobj[:domain] = ''
53
+ if sol.domain
54
+ dobj[:domain] += 'https://' unless options.raw
55
+ dobj[:domain] += sol.domain
56
+ end
57
+ sol.outf(dobj)
58
+ end
45
59
  elsif options.raw
46
60
  say(sol.domain)
47
61
  else
@@ -0,0 +1,272 @@
1
+ # Last Modified: 2017.08.31 /coding: utf-8
2
+ # frozen_string_literal: true
3
+
4
+ # Copyright © 2016-2017 Exosite LLC.
5
+ # License: MIT. See LICENSE.txt.
6
+ # vim:tw=0:ts=2:sw=2:et:ai
7
+
8
+ require 'highline'
9
+ require 'MrMurano/verbosing'
10
+ require 'MrMurano/Exchange'
11
+ require 'MrMurano/ReCommander'
12
+ require 'MrMurano/commands/business'
13
+
14
+ # *** Business commands: Exchange Elements
15
+ # ----------------------------------------
16
+
17
+ command :exchange do |c|
18
+ c.syntax = %(murano exchange)
19
+ c.summary = %(About IOT Exchange)
20
+ c.description = %(
21
+ Commands for working with IOT Exchange.
22
+ ).strip
23
+ c.project_not_required = true
24
+ c.subcmdgrouphelp = true
25
+
26
+ c.action do |_args, _options|
27
+ ::Commander::UI.enable_paging unless $cfg['tool.no-page']
28
+ say MrMurano::SubCmdGroupHelp.new(c).get_help
29
+ end
30
+ end
31
+
32
+ command 'exchange list' do |c|
33
+ c.syntax = %(murano exchange list [--options] [<name-or-ID>])
34
+ c.summary = %(List Exchange Elements)
35
+ c.description = %(
36
+ List Exchange Elements, either all of them, or those that are purchased or available.
37
+
38
+ Each Exchange Element is identified by an Element ID and a name.
39
+
40
+ Element status:
41
+
42
+ - added An Element that has been added to and enabled for your Business
43
+
44
+ - available An Element that can be added to and enabled for your Business
45
+
46
+ - available* An Element that you can use if you upgrade your Business tier
47
+
48
+ ).strip
49
+ c.project_not_required = true
50
+
51
+ cmd_table_output_add_options(c)
52
+
53
+ c.option '--[no-]added', 'Only show Elements that have been added to the Application'
54
+ c.option '--[no-]full', 'Show all fields'
55
+ c.option '--[no-]other', 'Show other fields: like type, tiers, tags, and action, and apiServiceName'
56
+
57
+ # Add --id and --name options.
58
+ cmd_options_add_id_and_name(c)
59
+
60
+ c.action do |args, options|
61
+ c.verify_arg_count!(args, 1)
62
+ cmd_defaults_id_and_name(options)
63
+
64
+ xchg = MrMurano::Exchange.new
65
+
66
+ elems, available, purchased = find_elements(xchg, options, args[0])
67
+ if options.added.nil?
68
+ show = elems
69
+ elsif options.added
70
+ show = purchased
71
+ else
72
+ show = available
73
+ end
74
+
75
+ headers, pruned = cmd_exchange_header_and_elems(show, options)
76
+
77
+ io = File.open(options.output, 'w') if options.output
78
+ xchg.outf(pruned, io) do |item, ios|
79
+ if options.idonly
80
+ ios.puts item
81
+ else
82
+ ios.puts "Found #{pruned.length} elements."
83
+ xchg.tabularize(
84
+ {
85
+ headers: headers.map(&:to_s),
86
+ rows: item,
87
+ },
88
+ ios,
89
+ )
90
+ end
91
+ end
92
+ io.close unless io.nil?
93
+ end
94
+ end
95
+ alias_command 'exchange list available', 'exchange list', '--no-added'
96
+ alias_command 'exchange list purchased', 'exchange list', '--added'
97
+
98
+ def find_elements(xchg, options, term)
99
+ filter_id = nil
100
+ filter_name = nil
101
+ filter_fuzzy = nil
102
+ if term
103
+ if options.id
104
+ filter_id = term
105
+ elsif options.name
106
+ filter_name = term
107
+ else
108
+ filter_fuzzy = term
109
+ end
110
+ end
111
+ xchg.elements(filter_id: filter_id, filter_name: filter_name, filter_fuzzy: filter_fuzzy)
112
+ end
113
+
114
+ def cmd_exchange_header_and_elems(elems, options)
115
+ # MAYBE/2017-08-31: If you `-c outformat=json`, each Element is a
116
+ # list of values, rather than a dictionary. Wouldn't the JSON be
117
+ # easier to consume if each Element was a dict, rather than list?
118
+ if options.idonly
119
+ headers = %i[elementId]
120
+ elems = elems.map(&:elementId)
121
+ elsif options.brief
122
+ headers = %i[elementId name]
123
+ headers += [:status] unless options.added
124
+ #elems = elems.map { |elem| [elem.elementId, elem.name] }
125
+ elems = elems.map { |elem| headers.map { |key| elem.send(key) } }
126
+ elsif options.full
127
+ headers = %i[elementId name status type apiServiceName tiers tags actions markdown]
128
+ all_hdrs = (elems[0] && elems[0].meta.keys) || []
129
+ all_hdrs.each do |chk|
130
+ headers.push(chk) unless headers.include?(chk)
131
+ end
132
+ #elems = elems.map { |elem| headers.map { |key| elem.meta[key] } }
133
+ elems = elems.map { |elem| headers.map { |key| elem.send(key) || '' } }
134
+ elsif options.other
135
+ # NOTE: Showing columns not displayed when --other not specified,
136
+ # except not showing :markdown, ever.
137
+ headers = %i[elementId type apiServiceName tiers tags actions]
138
+ #headers = %i[elementId type apiServiceName tiers tags actions markdown]
139
+ #elems = elems.map { |elem| headers.map { |key| elem.send(key) } }
140
+ elems = elems.map do |elem|
141
+ [
142
+ elem.elementId,
143
+ elem.type,
144
+ elem.apiServiceName,
145
+ #elem.tiers,
146
+ #elem.tiers.join(' | '),
147
+ elem.tiers.join("\n"),
148
+ #elem.tags,
149
+ #elem.tags.join(' | '),
150
+ elem.tags.join("\n"),
151
+ #elem.actions,
152
+ elem.actions.map { |actn| actn.map { |key, val| "#{key}: #{val}" }.join("\n") }.join("\n"),
153
+ #elem.markdown.gsub("\n", '\\n'),
154
+ ]
155
+ end
156
+ else
157
+ # 2017-08-28: There are 9 keys, and one of them -- :markdown -- is a
158
+ # lot of text, so rather than, e.g., elems[0].meta.keys, be selective.
159
+ headers = %i[elementId name]
160
+ headers += [:status] unless options.added
161
+ headers += [:description]
162
+ if $stdout.tty?
163
+ # Calculate how much room (how many characters) are left for the
164
+ # description column.
165
+ width_taken = 0
166
+ # rubocop:disable Performance/FixedSize
167
+ # "Do not compute the size of statically sized objects."
168
+ width_taken += '| '.length
169
+ # Calculate the width of each column except the last (:description).
170
+ headers[0..-2].each do |key|
171
+ elem_with_max = elems.max { |a, b| a.send(key).length <=> b.send(key).length }
172
+ width_taken += elem_with_max.send(key).length
173
+ width_taken += ' | '.length
174
+ end
175
+ width_taken += ' | '.length
176
+ term_width, _rows = HighLine::SystemExtensions.terminal_size
177
+ width_avail = term_width - width_taken
178
+ # MAGIC_NUMBER: Tweak/change this if you want. 20 char min feels
179
+ # about right: don't wrap if column would be narrow or negative.
180
+ width_avail = nil if width_avail < 20
181
+ else
182
+ width_avail = nil
183
+ end
184
+ #elems = elems.map { |elem| headers.map { |key| elem.send(key) } }
185
+ elems = elems.map do |elem|
186
+ headers.map do |key|
187
+ if !width_avail.nil? && key == :description
188
+ #elem.meta[key].scan(/.{1,#{width_avail}}/).join("\n")
189
+ full = elem.send(key)
190
+ parts = []
191
+ until full.empty?
192
+ # Split the description on a space before the max width.
193
+ # FIXME/2017-08-28: Need to test really long desc with no space.
194
+ part = full[0..width_avail]
195
+ full = full[(width_avail + 1)..-1] || ''
196
+ leftover = ''
197
+ part, _space, leftover = part.rpartition(' ') unless full.empty?
198
+ if part.empty?
199
+ part = leftover.to_s
200
+ leftover = ''
201
+ else
202
+ full = leftover.to_s + full
203
+ full = full
204
+ end
205
+ parts.push(part.strip)
206
+ end
207
+ parts.join("\n")
208
+ else
209
+ elem.send(key)
210
+ end
211
+ end
212
+ end
213
+ end
214
+ [headers, elems]
215
+ end
216
+
217
+ command 'exchange purchase' do |c|
218
+ c.syntax = %(murano exchange purchase [--options] <name-or-ID>)
219
+ c.summary = %(Add an Exchange Element to your Business)
220
+ c.description = %(
221
+ Add an Exchange Element to your Business.
222
+ ).strip
223
+ # It feels a little weird to not require a project, but all
224
+ # we need is the Business ID; this action does not apply to
225
+ # solutions.
226
+ c.project_not_required = true
227
+
228
+ # Add --id and --name options.
229
+ cmd_options_add_id_and_name(c)
230
+
231
+ c.action do |args, options|
232
+ c.verify_arg_count!(args, 1, ['Missing Element name or ID'])
233
+ cmd_defaults_id_and_name(options)
234
+
235
+ xchg = MrMurano::Exchange.new
236
+
237
+ # If the user specifies filter_id, we could try to fetch that Element
238
+ # directly (e.g., by calling exchange/<bizId>/element/<elemId>),
239
+ # but the response doesn't specify if the Element is purchased or not.
240
+ # So we grab everything from /element/ and /purchase/.
241
+
242
+ elems, _available, purchased = find_elements(xchg, options, args[0])
243
+ if elems.length > 1
244
+ idents = elems.map { |elem| "#{xchg.fancy_ticks(elem.name)} (#{elem.elementId})" }
245
+ idents[-1] = 'and ' + idents[-1]
246
+ xchg.warning(
247
+ 'Please be more specific: More than one matching element was found: ' \
248
+ "#{idents.join(', ')}"
249
+ )
250
+ exit 2
251
+ elsif elems.empty?
252
+ xchg.warning('No matching element was found.')
253
+ exit 2
254
+ elsif purchased.length == 1
255
+ # I.e., elems.status == :added
256
+ xchg.warning(
257
+ 'The specified element has already been purchased: ' \
258
+ "#{xchg.fancy_ticks(purchased[0].name)} (#{purchased[0].elementId})"
259
+ )
260
+ exit 2
261
+ elsif elems.first.status == :upgrade
262
+ xchg.warning('Please upgrade your Business to add this Element. Visit:')
263
+ xchg.warning(' https://www.exosite.io/business/settings/upgrade')
264
+ exit 2
265
+ end
266
+
267
+ xchg.purchase(elems.first.elementId)
268
+ end
269
+ end
270
+ alias_command 'exchange add', 'exchange purchase'
271
+ alias_command 'exchange buy', 'exchange purchase'
272
+