MuranoCLI 3.0.1 → 3.0.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.
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
+