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

Sign up to get free protection for your applications and to get access to all the features.
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
+