MuranoCLI 3.2.0.beta.1 → 3.2.0.beta.5

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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -1
  3. data/.trustme.plugin +137 -0
  4. data/.trustme.sh +217 -117
  5. data/.trustme.vim +9 -3
  6. data/Gemfile +9 -3
  7. data/MuranoCLI.gemspec +8 -5
  8. data/Rakefile +1 -0
  9. data/dockers/Dockerfile.2.2.9 +6 -3
  10. data/dockers/Dockerfile.2.3.6 +6 -3
  11. data/dockers/Dockerfile.2.4.3 +6 -3
  12. data/dockers/Dockerfile.2.5.0 +6 -3
  13. data/dockers/Dockerfile.GemRelease +10 -8
  14. data/dockers/Dockerfile.m4 +23 -5
  15. data/dockers/docker-test.sh +65 -28
  16. data/docs/completions/murano_completion-bash +751 -57
  17. data/docs/develop.rst +10 -9
  18. data/lib/MrMurano/AccountBase.rb +95 -6
  19. data/lib/MrMurano/Commander-Entry.rb +9 -4
  20. data/lib/MrMurano/Config-Migrate.rb +2 -0
  21. data/lib/MrMurano/Config.rb +94 -26
  22. data/lib/MrMurano/Content.rb +1 -1
  23. data/lib/MrMurano/Exchange.rb +77 -42
  24. data/lib/MrMurano/Gateway.rb +1 -1
  25. data/lib/MrMurano/HttpAuthed.rb +20 -7
  26. data/lib/MrMurano/Logs.rb +10 -1
  27. data/lib/MrMurano/ProjectFile.rb +1 -1
  28. data/lib/MrMurano/ReCommander.rb +129 -73
  29. data/lib/MrMurano/Solution-ServiceConfig.rb +18 -11
  30. data/lib/MrMurano/Solution-Services.rb +78 -50
  31. data/lib/MrMurano/Solution-Users.rb +1 -1
  32. data/lib/MrMurano/Solution.rb +13 -63
  33. data/lib/MrMurano/SyncUpDown-Core.rb +185 -77
  34. data/lib/MrMurano/SyncUpDown-Item.rb +29 -4
  35. data/lib/MrMurano/SyncUpDown.rb +11 -11
  36. data/lib/MrMurano/Webservice-Cors.rb +1 -1
  37. data/lib/MrMurano/Webservice-Endpoint.rb +28 -17
  38. data/lib/MrMurano/Webservice-File.rb +103 -43
  39. data/lib/MrMurano/commands/domain.rb +1 -0
  40. data/lib/MrMurano/commands/element.rb +585 -0
  41. data/lib/MrMurano/commands/exchange.rb +211 -204
  42. data/lib/MrMurano/commands/gb.rb +1 -0
  43. data/lib/MrMurano/commands/globals.rb +17 -7
  44. data/lib/MrMurano/commands/init.rb +115 -101
  45. data/lib/MrMurano/commands/keystore.rb +1 -1
  46. data/lib/MrMurano/commands/logs.rb +2 -1
  47. data/lib/MrMurano/commands/postgresql.rb +17 -7
  48. data/lib/MrMurano/commands/service.rb +572 -0
  49. data/lib/MrMurano/commands/show.rb +7 -3
  50. data/lib/MrMurano/commands/solution.rb +2 -1
  51. data/lib/MrMurano/commands/solution_picker.rb +31 -15
  52. data/lib/MrMurano/commands/status.rb +205 -169
  53. data/lib/MrMurano/commands/sync.rb +70 -38
  54. data/lib/MrMurano/commands/token.rb +59 -14
  55. data/lib/MrMurano/commands/usage.rb +1 -0
  56. data/lib/MrMurano/commands.rb +2 -0
  57. data/lib/MrMurano/hash.rb +91 -0
  58. data/lib/MrMurano/http.rb +55 -6
  59. data/lib/MrMurano/makePretty.rb +47 -0
  60. data/lib/MrMurano/optparse.rb +60 -45
  61. data/lib/MrMurano/variegated/TruthyFalsey.rb +48 -0
  62. data/lib/MrMurano/variegated/ruby_dig.rb +64 -0
  63. data/lib/MrMurano/verbosing.rb +113 -3
  64. data/lib/MrMurano/version.rb +1 -1
  65. data/spec/Account_spec.rb +34 -20
  66. data/spec/Business_spec.rb +12 -9
  67. data/spec/Config_spec.rb +7 -1
  68. data/spec/Content_spec.rb +17 -1
  69. data/spec/GatewayBase_spec.rb +5 -2
  70. data/spec/GatewayDevice_spec.rb +4 -2
  71. data/spec/GatewayResource_spec.rb +4 -1
  72. data/spec/GatewaySettings_spec.rb +4 -1
  73. data/spec/HttpAuthed_spec.rb +73 -0
  74. data/spec/Http_spec.rb +32 -35
  75. data/spec/ProjectFile_spec.rb +1 -1
  76. data/spec/Solution-ServiceConfig_spec.rb +4 -1
  77. data/spec/Solution-ServiceEventHandler_spec.rb +6 -3
  78. data/spec/Solution-ServiceModules_spec.rb +4 -1
  79. data/spec/Solution-UsersRoles_spec.rb +4 -1
  80. data/spec/Solution_spec.rb +4 -1
  81. data/spec/SyncUpDown_spec.rb +1 -1
  82. data/spec/Webservice-Cors_spec.rb +4 -1
  83. data/spec/Webservice-Endpoint_spec.rb +9 -6
  84. data/spec/Webservice-File_spec.rb +17 -4
  85. data/spec/Webservice-Setting_spec.rb +6 -2
  86. data/spec/_workspace.rb +2 -0
  87. data/spec/cmd_common.rb +42 -13
  88. data/spec/cmd_content_spec.rb +17 -7
  89. data/spec/cmd_device_spec.rb +1 -1
  90. data/spec/cmd_domain_spec.rb +2 -2
  91. data/spec/cmd_element_spec.rb +400 -0
  92. data/spec/cmd_exchange_spec.rb +2 -2
  93. data/spec/cmd_init_spec.rb +59 -25
  94. data/spec/cmd_keystore_spec.rb +6 -3
  95. data/spec/cmd_link_spec.rb +10 -5
  96. data/spec/cmd_logs_spec.rb +1 -1
  97. data/spec/cmd_setting_application_spec.rb +18 -15
  98. data/spec/cmd_setting_product_spec.rb +7 -7
  99. data/spec/cmd_status_spec.rb +27 -17
  100. data/spec/cmd_syncdown_application_spec.rb +30 -3
  101. data/spec/cmd_syncdown_both_spec.rb +72 -18
  102. data/spec/cmd_syncup_spec.rb +71 -5
  103. data/spec/cmd_token_spec.rb +2 -2
  104. data/spec/cmd_usage_spec.rb +2 -2
  105. data/spec/dry_run_formatter.rb +27 -0
  106. data/spec/fixtures/dumped_config +8 -0
  107. data/spec/fixtures/exchange_element/element-show.json +1 -0
  108. data/spec/fixtures/exchange_element/swagger-mur-6407__10k.yaml +282 -0
  109. data/spec/fixtures/exchange_element/swagger-mur-6407__20k.yaml +588 -0
  110. data/spec/variegated_TruthyFalsey_spec.rb +29 -0
  111. metadata +51 -25
@@ -0,0 +1,585 @@
1
+ # Copyright © 2016-2017 Exosite LLC. All Rights Reserved
2
+ # License: PROPRIETARY. See LICENSE.txt.
3
+ # frozen_string_literal: true
4
+
5
+ # vim:tw=0:ts=2:sw=2:et:ai
6
+ # Unauthorized copying of this file is strictly prohibited.
7
+
8
+ require 'highline'
9
+ require 'pathname'
10
+ require 'tempfile'
11
+ require 'tty-editor'
12
+ require 'MrMurano/hash'
13
+ require 'MrMurano/makePretty'
14
+ require 'MrMurano/verbosing'
15
+ require 'MrMurano/Exchange'
16
+ require 'MrMurano/ReCommander'
17
+ require 'MrMurano/commands/business'
18
+ require 'MrMurano/commands/exchange'
19
+ require 'MrMurano/variegated/ruby_dig'
20
+
21
+ # NOTE: For details on the BizAPI Exchange Element API, see:
22
+ #
23
+ # https://docs.google.com/document/d/1VlFmkiNcBK9AX6BpgV-E_5EDGgt0K_L5exJcgv6gEDQ/edit#heading=h.l6fiheqnpa08
24
+
25
+ class ElementCmd
26
+ include MrMurano::Verbose
27
+
28
+ def initialize
29
+ reset_state
30
+ end
31
+
32
+ def reset_state
33
+ @edit_fields = {}
34
+ @updated_obj = {}
35
+ @use_editor = false
36
+ @input_path = nil
37
+ @input_data = nil
38
+ end
39
+
40
+ # *** element-help command
41
+
42
+ def command_element_help(cmd)
43
+ cmd.syntax = %(murano element)
44
+ cmd.summary = %(IoT Marketplace Exchange Element commands)
45
+ cmd.description = %(
46
+ Commands for working with IoT Marketplace Exchange Elements.
47
+ ).strip
48
+ cmd.project_not_required = true
49
+ cmd.subcmdgrouphelp = true
50
+
51
+ cmd.action do |_args, _options|
52
+ ::Commander::UI.enable_paging unless $cfg['tool.no-page']
53
+ say MrMurano::SubCmdGroupHelp.new(cmd).get_help
54
+ reset_state
55
+ end
56
+ end
57
+
58
+ def verify_args_id_or_name!(cmd, args, options, max_args: 1)
59
+ cmd.verify_arg_count!(args, max_args, ['Missing Element name or ID'])
60
+ cmd_defaults_id_and_name(options)
61
+ end
62
+
63
+ def elems_must_find_one!(args, options)
64
+ xchg = MrMurano::Exchange.new
65
+ xchg.must_business_id!
66
+
67
+ exchange_cmd = ExchangeCmd.new
68
+
69
+ elems, _available, _purchased = exchange_cmd.find_elements(
70
+ xchg, options, args[0], skip_purchased: true,
71
+ )
72
+
73
+ exchange_cmd.elems_must_found_one!(elems, xchg)
74
+ end
75
+
76
+ # *** element-show command
77
+
78
+ NOT_A_BAD_ORDER_ELEMENT_KEYS = %i[
79
+ bizid
80
+ elementId
81
+ name
82
+ tags
83
+ type
84
+ contact
85
+ source
86
+ specs
87
+ description
88
+ markdown
89
+ image
90
+ attachment
91
+
92
+ approval
93
+ active
94
+ managed
95
+ pinned
96
+ access
97
+ tiers
98
+ ].freeze
99
+
100
+ def command_element_show(cmd)
101
+ cmd.syntax = %(murano element show [--options] <name-or-ID>)
102
+ cmd.summary = %(Show details about an IoT Marketplace Exchange Element)
103
+ cmd.description = %(
104
+ Show details about an IoT Marketplace Exchange Element.
105
+ ).strip
106
+ # We don't need to be in a project directory; we just need the Biz ID.
107
+ cmd.project_not_required = true
108
+
109
+ # Not too DRY: See also cmd_table_output_add_options(cmd), which also
110
+ # adds --idonly and --[no-]brief.
111
+ cmd.option '-o', '--output FILE', 'Download to file instead of STDOUT'
112
+
113
+ cmd.option '--[no-]truncate', 'Truncate longs lines...'
114
+ cmd.option '--[no-]wrap', 'Wrap long lines...'
115
+
116
+ cmd.example %(Show table of Exchange Element fields, wrapped nicely to fit terminal
117
+ ), %(murano element show abcdef1234567890abcdef1234567890abcdef12 --wrap)
118
+
119
+ cmd.example %(Store Exchange Element record as JSON in local file
120
+ ), %(murano element show 'remote condition monitoring' --json > rcm.json)
121
+
122
+ # Add --id and --name options.
123
+ cmd_options_add_id_and_name(cmd)
124
+
125
+ cmd_options_pretty_format_option(cmd)
126
+
127
+ cmd.action do |args, options|
128
+ verify_args_id_or_name!(cmd, args, options)
129
+ sole_elem = elems_must_find_one!(args, options)
130
+
131
+ orig_elem, flatkeys, val_lkup = objectify_elem(sole_elem, options)
132
+
133
+ io = File.open(options.output, 'w') if options.output
134
+ outf(orig_elem, io, pretty: options.pretty) do |elem, ios|
135
+ if $cfg['tool.outformat'] =~ /csv/i
136
+ headers, row_lkup = prepare_hash_csv(elem)
137
+ else
138
+ # Outformat is table. Make tall, not wide.
139
+ headers = %w[key value]
140
+ # Sort by top-level keys first, i.e., group objects, at least.
141
+ flatkeys = sort_key_hier(flatkeys, elem.keys)
142
+ row_lkup = Hash[flatkeys.collect { |key| [key, val_lkup[key]] }]
143
+ end
144
+ tabularize({ headers: headers, rows: row_lkup }, ios)
145
+ end
146
+ reset_state
147
+ end
148
+ end
149
+
150
+ def cmd_options_pretty_format_option(cmd)
151
+ cmd.option('-p', '--pretty', %(Whether to pretty-print --json))
152
+ end
153
+
154
+ def objectify_elem(elem, options)
155
+ lkup = objectify_elem_lkup(elem)
156
+ flatkeys = objectify_elem_flat(lkup)
157
+ width_avail = objectify_elem_width(flatkeys, options)
158
+ val_lkup = objectify_elem_values(flatkeys, lkup, width_avail, options)
159
+ [lkup, flatkeys, val_lkup]
160
+ end
161
+
162
+ def objectify_elem_lkup(elem)
163
+ lkup = HashDiggable.new(elem.meta)
164
+ lkup.default_proc = proc do |hash, key|
165
+ warning %(Not a symbol!: #{key}) unless key.is_a? Symbol
166
+ hash[key] = Hash.new(&hash.default_proc)
167
+ end
168
+ lkup
169
+ end
170
+
171
+ def objectify_elem_flat(lkup)
172
+ flatkeys = Hash.flat_hash(lkup).keys.map { |hier| hier.join('.') }
173
+ flatkeys.sort
174
+ end
175
+
176
+ def objectify_elem_width(flatkeys, options)
177
+ # This is similar to Pretties.width_last_column
178
+ width_avail = -1
179
+ if options.truncate || options.wrap
180
+ width_taken = 0
181
+ # Account for the left and right borders.
182
+ # rubocop:disable Performance/FixedSize
183
+ width_taken += 2 * ('| '.length)
184
+ # And account for the middle column split.
185
+ width_taken += ' | '.length
186
+ width_taken += flatkeys.max_by(&:length).length
187
+ term_width, _rows = HighLine::SystemExtensions.terminal_size
188
+ width_avail = term_width - width_taken
189
+ end
190
+ width_avail
191
+ end
192
+
193
+ def objectify_elem_values(flatkeys, lkup, width_avail, options)
194
+ val_lkup = {}
195
+ flatkeys.each do |key|
196
+ layers = key.split('.').map(&:to_sym)
197
+ val = lkup.dig(*layers).to_s
198
+ if width_avail > 0
199
+ if options.truncate
200
+ val.slice!(width_avail..-1)
201
+ elsif options.wrap
202
+ val = MrMurano::Pretties.split_text_on_whitespace(val, width_avail)
203
+ end
204
+ end
205
+ # Store the flattened key-value if a flat lookup, for the table maker.
206
+ val_lkup[key] = val
207
+ end
208
+ val_lkup
209
+ end
210
+
211
+ def sort_key_hier(flattened_keys, top_level_keys)
212
+ sort_order = (NOT_A_BAD_ORDER_ELEMENT_KEYS + top_level_keys.sort).map(&:to_s).uniq
213
+ flattened_keys.sort_by do |key|
214
+ sort_order.index do |top|
215
+ key.downcase.start_with?(top.downcase)
216
+ end
217
+ end
218
+ end
219
+
220
+ # *** element-edit command
221
+
222
+ EXCHANGE_ELEMENT_FIELD_DESC = {
223
+ # Not editable: bizid
224
+ # Not editable: elementId
225
+ name: ['elem-name', 'Element name'],
226
+ # MAYBE/2018-04-26: (lb): Web UI does not reveal tags. Should the CLI?
227
+ tags: ['', 'Element tags'], # ["...",...]
228
+ contact: ['', 'Element contact information'],
229
+ specs: [
230
+ '',
231
+ (
232
+ "Included Capabilities\n\n" \
233
+ 'List exchange element compatibilities, ' \
234
+ 'versions, and or other technically relevant aspects'
235
+ ),
236
+ ],
237
+ description: ['', 'Short description'],
238
+ markdown: ['', 'Fill item description'],
239
+ # Dynamic options (use -e ...):
240
+ # source.{from|name|url}
241
+ # Uneditable strings:
242
+ # type = [download|service|product|application|contactSales}
243
+ # tiers = [free|developer|professional|enterprise]
244
+ # access = [public|...]
245
+ # approval = [approved|...]
246
+ # Uneditable booleans:
247
+ # active
248
+ # pinned
249
+ # managed
250
+ # publishToBusinessNetwork
251
+ # Not yet/Not going to be supported options:
252
+ # image.detail.{color|filename|type|url}
253
+ # image.thumbnail.{color|filename|type|url}
254
+ }.freeze
255
+
256
+ EXCHANGE_ELEMENT_NOT_ALLOWED = %i[
257
+ tiers
258
+ approval
259
+ ].freeze
260
+
261
+ def command_element_edit(cmd)
262
+ cmd.syntax = %(murano element edit [--options] <name-or-ID> [file])
263
+ cmd.summary = %(Edit details about an IoT Marketplace Exchange Element)
264
+ cmd.description = %(
265
+ Edit details about an IoT Marketplace Exchange Element.
266
+ ).strip
267
+ # We don't need to be in a project directory; we just need the Biz ID.
268
+ cmd.project_not_required = true
269
+
270
+ # Add --id and --name options.
271
+ cmd_options_add_id_and_name(cmd)
272
+
273
+ cmd_options_add_exchange_element_fields(cmd)
274
+
275
+ cmd_options_add_outformat_plain(cmd)
276
+
277
+ cmd.example %(Upload Exchange Element record from JSON stored in local file
278
+ ), %(murano element edit 'remote condition monitoring' rcm.json)
279
+
280
+ cmd.example %(Edit single field of Exchange Element record
281
+ ), %(murano element edit 'my element' -e image.thumbnail.color=#5C5D60)
282
+
283
+ cmd.example %(Edit single sub dictionary of Exchange Element record using Yaml file
284
+ ), %(murano element edit -e source -- 'my element' element-source.yaml)
285
+
286
+ cmd.example %(Edit single sub dictionary of Exchange Element record using JSON file
287
+ ), %(murano element edit -e source=@element-source.yaml 'my element')
288
+
289
+ cmd.example %(Edit Exchange Element fields using your ${EDITOR}
290
+ ), %(murano element edit -e -- <Element_ID_or_Name>)
291
+
292
+ cmd.action do |args, options|
293
+ begin
294
+ cmd_edit_execute(cmd, args, options)
295
+ ensure
296
+ # This is for RSpec, so the command instance resets itself between runs.
297
+ reset_state
298
+ end
299
+ end
300
+ end
301
+
302
+ def cmd_edit_execute(cmd, args, options)
303
+ verify_args_id_or_name!(cmd, args, options, max_args: 2)
304
+ file_must_find_maybe(args[1])
305
+ options_must_specify_edits!
306
+ edit_fields_load_input_files
307
+ sole_elem = elems_must_find_one!(args, options)
308
+ update_elem_fields(sole_elem)
309
+ xchg = MrMurano::Exchange.new
310
+ xchg.put(sole_elem.elementId, @updated_obj)
311
+ end
312
+
313
+ def cmd_options_add_exchange_element_fields(cmd)
314
+ cmd_options_add_fields_static(cmd)
315
+ cmd_options_add_fields_dynamic(cmd)
316
+ end
317
+
318
+ def cmd_options_add_fields_static(cmd)
319
+ # Add --options from EXCHANGE_ELEMENT_FIELD_DESC but sort them.
320
+ NOT_A_BAD_ORDER_ELEMENT_KEYS.each do |field|
321
+ field_desc = EXCHANGE_ELEMENT_FIELD_DESC[field]
322
+ next if field_desc.nil?
323
+ switch = !field_desc[0].empty? && field_desc[0] || field.to_s
324
+ detail = !field_desc[1].empty? && field_desc[1] || field.to_s
325
+ cmd.option(
326
+ "--#{switch} [VALUE]", detail
327
+ ) do |param|
328
+ @edit_fields[field.to_s] = param
329
+ end
330
+ end
331
+ end
332
+
333
+ def cmd_options_add_fields_dynamic(cmd)
334
+ cmd.option(
335
+ '-e', '--edit [KEY[=VALUE]]', %(Set the Element field named KEY to VALUE)
336
+ ) do |param|
337
+ if param.nil?
338
+ # param.nil? means user did not supply key[=val].
339
+ @edit_fields[''] = nil
340
+ else
341
+ # Otherwise: a=b :> ['a', 'b'] / a= :> ['a', ''] / a :> ['a', nil]
342
+ key, value = param.split('=', 2)
343
+ @edit_fields[key] = value
344
+ end
345
+ end
346
+ end
347
+
348
+ def cmd_options_add_outformat_plain(cmd)
349
+ cmd.option(
350
+ '--plain',
351
+ 'Do not decode input file(s) (e.g., if named specs.yaml, import as plaintext)'
352
+ ) do
353
+ $cfg['tool.outformat'] = 'text'
354
+ end
355
+ end
356
+
357
+ def file_must_find_maybe(path)
358
+ return if path.nil?
359
+ @input_path = Pathname.new(path) unless path.is_a?(Pathname)
360
+ return if @input_path.exist?
361
+ error %(Input file not found: #{@input_path})
362
+ exit 1
363
+ end
364
+
365
+ def options_must_specify_edits!
366
+ n_kvals = { n_keys: 0, n_vals: 0 }
367
+ options_edits_count(n_kvals)
368
+ options_edits_verify!(n_kvals)
369
+ end
370
+
371
+ def options_edits_count(n_kvals)
372
+ @edit_fields.each do |_key, val|
373
+ n_kvals[:n_keys] += 1
374
+ n_kvals[:n_vals] += 1 unless val.nil?
375
+ end
376
+ end
377
+
378
+ def options_edits_verify!(n_kvals)
379
+ if @input_path.nil?
380
+ # See if all values are specified, or if we should bring up the EDITOR.
381
+ if n_kvals[:n_keys] > 1
382
+ if n_kvals[:n_keys] != n_kvals[:n_vals]
383
+ error %(
384
+ Please specify at most one field when not specifing all field values.
385
+ ).strip
386
+ exit 2
387
+ end
388
+ elsif n_kvals[:n_keys] == 0
389
+ error('Please specify one or more -e/--edit options, or an input file.')
390
+ exit 2
391
+ else
392
+ @use_editor = n_kvals[:n_vals].zero?
393
+ end
394
+ elsif n_kvals[:n_keys] > 1
395
+ error('Please specify at most a single field when specifing an input file.')
396
+ exit 2
397
+ elsif n_kvals[:n_vals] > 0
398
+ error('Please do not specify a value when specifing an input file.')
399
+ exit 2
400
+ end
401
+ end
402
+
403
+ def edit_fields_load_input_files
404
+ set_fields_from_files_from_options
405
+ set_field_from_file_from_positional
406
+ end
407
+
408
+ def set_fields_from_files_from_options
409
+ @edit_fields.update(@edit_fields) do |_key, value, _other|
410
+ next value unless value_specifies_file_input?(value)
411
+ load_input_maybe(value)
412
+ end
413
+ end
414
+
415
+ def set_field_from_file_from_positional
416
+ return if @input_path.nil?
417
+ @input_data = read_hashf!(@input_path)
418
+ @input_data.deep_symbolize_keys! if @input_data.is_a? Hash
419
+ end
420
+
421
+ def update_elem_fields(elem)
422
+ @updated_obj = HashDiggable.new(elem.meta)
423
+ n_edits = 0
424
+ # When @edit_fields == { '' => '' }, BizAPI replies:
425
+ # Request Failed: 400: [400] child "type" fails because ["type" is required]
426
+ if @use_editor
427
+ n_edits += update_elem_fields_from_editor
428
+ else
429
+ # Verify that at least one option value differs from what's currently set.
430
+ n_edits += update_elem_fields_from_options
431
+ n_edits += update_elem_fields_from_input_f
432
+ end
433
+ if n_edits.zero?
434
+ warning('No new field values specified to update.')
435
+ exit 0
436
+ end
437
+ @updated_obj.reject! { |key, _val| EXCHANGE_ELEMENT_NOT_ALLOWED.include?(key) }
438
+ end
439
+
440
+ def update_elem_fields_from_editor
441
+ keys, hash_val = prepare_field_keys_and_value
442
+ tmpf, fmt = write_editable_tmp_file(hash_val)
443
+ user_editor_interact(tmpf)
444
+ edit_val = consume_edited_tmp_file(tmpf, fmt)
445
+ edit_val.deep_symbolize_keys! if edit_val.is_a? Hash
446
+ update_field_maybe!(keys, edit_val, hash_val)
447
+ end
448
+
449
+ def prepare_field_keys_and_value
450
+ field = @edit_fields.first[0]
451
+ keys = []
452
+ keys = field.split('.').map(&:to_sym) unless field.to_s.empty?
453
+ if !keys.nil? && !keys.empty?
454
+ hash_val = @updated_obj.dig_safe(*keys)
455
+ else
456
+ hash_val = @updated_obj
457
+ end
458
+ [keys, hash_val]
459
+ end
460
+
461
+ def write_editable_tmp_file(hash_val)
462
+ fmt = outformat_engine(nil, hash_val)
463
+ tmpf = Tempfile.new(["murcli_#{@updated_obj[:elementId]}-", ".#{fmt}"])
464
+ dump_output_file(hash_val, tmpf, fmt, pretty: true)
465
+ tmpf.close
466
+ [tmpf, fmt]
467
+ end
468
+
469
+ def outformat_engine(fext, hash_val=nil)
470
+ if !fext.nil?
471
+ super(fext)
472
+ elsif hash_val.is_a? Hash
473
+ super($cfg['tool.outformat'])
474
+ else
475
+ :plain
476
+ end
477
+ end
478
+
479
+ def user_editor_interact(tmpf)
480
+ TTY::Editor.open(tmpf.path)
481
+ end
482
+
483
+ def consume_edited_tmp_file(tmpf, fmt)
484
+ tmpf.open
485
+ edit_val = load_input_file(tmpf, fmt)
486
+ tmpf.unlink
487
+ edit_val
488
+ end
489
+
490
+ def update_field_maybe!(keys, edit_val, hash_val)
491
+ n_edits = 0
492
+ if edit_val != hash_val
493
+ if !keys.nil? && !keys.empty?
494
+ @updated_obj.fill_safe(edit_val, *keys)
495
+ else
496
+ @updated_obj = obj_must_be_hash!(edit_val)
497
+ end
498
+ n_edits += 1
499
+ end
500
+ n_edits
501
+ end
502
+
503
+ def update_elem_fields_from_options
504
+ n_edits = 0
505
+ @edit_fields.each_pair do |key, value|
506
+ next if value.nil?
507
+ keys = key.split('.').map(&:to_sym) unless key.nil?
508
+ if !keys.nil? && !keys.empty?
509
+ old_val = @updated_obj.dig_safe(*keys)
510
+ else
511
+ old_val = @updated_obj
512
+ end
513
+ n_edits += update_field_maybe!(keys, value, old_val)
514
+ end
515
+ n_edits
516
+ end
517
+
518
+ def obj_must_be_hash!(obj)
519
+ if obj.is_a? Hash
520
+ obj
521
+ elsif obj.empty?
522
+ {}
523
+ else
524
+ begin
525
+ parsed = JSON.parse(obj)
526
+ rescue JSON::ParserError => err
527
+ error %(The document object is not a Hash: #{obj})
528
+ error err.to_s
529
+ exit 2
530
+ end
531
+ return parsed if parsed.is_a? Hash
532
+ end
533
+ end
534
+
535
+ def update_elem_fields_from_input_f
536
+ n_edits = 0
537
+ return n_edits if @input_path.nil?
538
+ if (@edit_fields.length > 1) && $cfg['tool.developer']
539
+ warning %(Unexpected: more than one field specified)
540
+ end
541
+ keys = @edit_fields.first[0].split('.').map(&:to_sym) unless @edit_fields.empty?
542
+ if !keys.nil? && !keys.empty?
543
+ old_val = @updated_obj.dig_safe(*keys)
544
+ if old_val != @input_data
545
+ @updated_obj.fill_safe(@input_data, *keys)
546
+ n_edits += 1
547
+ end
548
+ elsif @updated_obj != @input_data
549
+ @updated_obj = @input_data
550
+ n_edits += 1
551
+ end
552
+ n_edits
553
+ end
554
+
555
+ def value_specifies_file_input?(field_value)
556
+ field_value =~ /^@(?!@)/
557
+ end
558
+
559
+ def load_input_maybe(field_value)
560
+ return field_value.gsub(/^@@/, '@') unless value_specifies_file_input?(field_value)
561
+ # If the -e field=value starts with '@', e.g., input=@in.json,
562
+ # try to load the field value from the file.
563
+ path = Pathname.new(field_value.slice(1..-1))
564
+ read_hashf!(path)
565
+ end
566
+
567
+ def read_hashf!(path, fmt=nil)
568
+ fmt = :plain if fmt.nil? && ($cfg['tool.outformat'] == 'text')
569
+ @input_data = super(path, fmt)
570
+ end
571
+ end
572
+
573
+ def wire_cmd_element
574
+ element_cmd = ElementCmd.new
575
+
576
+ command(:element) { |cmd| element_cmd.command_element_help(cmd) }
577
+
578
+ command('element show') { |cmd| element_cmd.command_element_show(cmd) }
579
+
580
+ command('element edit') { |cmd| element_cmd.command_element_edit(cmd) }
581
+ alias_command 'element update', 'element edit'
582
+ end
583
+
584
+ wire_cmd_element
585
+