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.
- checksums.yaml +4 -4
- data/.rubocop.yml +4 -1
- data/.trustme.plugin +137 -0
- data/.trustme.sh +217 -117
- data/.trustme.vim +9 -3
- data/Gemfile +9 -3
- data/MuranoCLI.gemspec +8 -5
- data/Rakefile +1 -0
- data/dockers/Dockerfile.2.2.9 +6 -3
- data/dockers/Dockerfile.2.3.6 +6 -3
- data/dockers/Dockerfile.2.4.3 +6 -3
- data/dockers/Dockerfile.2.5.0 +6 -3
- data/dockers/Dockerfile.GemRelease +10 -8
- data/dockers/Dockerfile.m4 +23 -5
- data/dockers/docker-test.sh +65 -28
- data/docs/completions/murano_completion-bash +751 -57
- data/docs/develop.rst +10 -9
- data/lib/MrMurano/AccountBase.rb +95 -6
- data/lib/MrMurano/Commander-Entry.rb +9 -4
- data/lib/MrMurano/Config-Migrate.rb +2 -0
- data/lib/MrMurano/Config.rb +94 -26
- data/lib/MrMurano/Content.rb +1 -1
- data/lib/MrMurano/Exchange.rb +77 -42
- data/lib/MrMurano/Gateway.rb +1 -1
- data/lib/MrMurano/HttpAuthed.rb +20 -7
- data/lib/MrMurano/Logs.rb +10 -1
- data/lib/MrMurano/ProjectFile.rb +1 -1
- data/lib/MrMurano/ReCommander.rb +129 -73
- data/lib/MrMurano/Solution-ServiceConfig.rb +18 -11
- data/lib/MrMurano/Solution-Services.rb +78 -50
- data/lib/MrMurano/Solution-Users.rb +1 -1
- data/lib/MrMurano/Solution.rb +13 -63
- data/lib/MrMurano/SyncUpDown-Core.rb +185 -77
- data/lib/MrMurano/SyncUpDown-Item.rb +29 -4
- data/lib/MrMurano/SyncUpDown.rb +11 -11
- data/lib/MrMurano/Webservice-Cors.rb +1 -1
- data/lib/MrMurano/Webservice-Endpoint.rb +28 -17
- data/lib/MrMurano/Webservice-File.rb +103 -43
- data/lib/MrMurano/commands/domain.rb +1 -0
- data/lib/MrMurano/commands/element.rb +585 -0
- data/lib/MrMurano/commands/exchange.rb +211 -204
- data/lib/MrMurano/commands/gb.rb +1 -0
- data/lib/MrMurano/commands/globals.rb +17 -7
- data/lib/MrMurano/commands/init.rb +115 -101
- data/lib/MrMurano/commands/keystore.rb +1 -1
- data/lib/MrMurano/commands/logs.rb +2 -1
- data/lib/MrMurano/commands/postgresql.rb +17 -7
- data/lib/MrMurano/commands/service.rb +572 -0
- data/lib/MrMurano/commands/show.rb +7 -3
- data/lib/MrMurano/commands/solution.rb +2 -1
- data/lib/MrMurano/commands/solution_picker.rb +31 -15
- data/lib/MrMurano/commands/status.rb +205 -169
- data/lib/MrMurano/commands/sync.rb +70 -38
- data/lib/MrMurano/commands/token.rb +59 -14
- data/lib/MrMurano/commands/usage.rb +1 -0
- data/lib/MrMurano/commands.rb +2 -0
- data/lib/MrMurano/hash.rb +91 -0
- data/lib/MrMurano/http.rb +55 -6
- data/lib/MrMurano/makePretty.rb +47 -0
- data/lib/MrMurano/optparse.rb +60 -45
- data/lib/MrMurano/variegated/TruthyFalsey.rb +48 -0
- data/lib/MrMurano/variegated/ruby_dig.rb +64 -0
- data/lib/MrMurano/verbosing.rb +113 -3
- data/lib/MrMurano/version.rb +1 -1
- data/spec/Account_spec.rb +34 -20
- data/spec/Business_spec.rb +12 -9
- data/spec/Config_spec.rb +7 -1
- data/spec/Content_spec.rb +17 -1
- data/spec/GatewayBase_spec.rb +5 -2
- data/spec/GatewayDevice_spec.rb +4 -2
- data/spec/GatewayResource_spec.rb +4 -1
- data/spec/GatewaySettings_spec.rb +4 -1
- data/spec/HttpAuthed_spec.rb +73 -0
- data/spec/Http_spec.rb +32 -35
- data/spec/ProjectFile_spec.rb +1 -1
- data/spec/Solution-ServiceConfig_spec.rb +4 -1
- data/spec/Solution-ServiceEventHandler_spec.rb +6 -3
- data/spec/Solution-ServiceModules_spec.rb +4 -1
- data/spec/Solution-UsersRoles_spec.rb +4 -1
- data/spec/Solution_spec.rb +4 -1
- data/spec/SyncUpDown_spec.rb +1 -1
- data/spec/Webservice-Cors_spec.rb +4 -1
- data/spec/Webservice-Endpoint_spec.rb +9 -6
- data/spec/Webservice-File_spec.rb +17 -4
- data/spec/Webservice-Setting_spec.rb +6 -2
- data/spec/_workspace.rb +2 -0
- data/spec/cmd_common.rb +42 -13
- data/spec/cmd_content_spec.rb +17 -7
- data/spec/cmd_device_spec.rb +1 -1
- data/spec/cmd_domain_spec.rb +2 -2
- data/spec/cmd_element_spec.rb +400 -0
- data/spec/cmd_exchange_spec.rb +2 -2
- data/spec/cmd_init_spec.rb +59 -25
- data/spec/cmd_keystore_spec.rb +6 -3
- data/spec/cmd_link_spec.rb +10 -5
- data/spec/cmd_logs_spec.rb +1 -1
- data/spec/cmd_setting_application_spec.rb +18 -15
- data/spec/cmd_setting_product_spec.rb +7 -7
- data/spec/cmd_status_spec.rb +27 -17
- data/spec/cmd_syncdown_application_spec.rb +30 -3
- data/spec/cmd_syncdown_both_spec.rb +72 -18
- data/spec/cmd_syncup_spec.rb +71 -5
- data/spec/cmd_token_spec.rb +2 -2
- data/spec/cmd_usage_spec.rb +2 -2
- data/spec/dry_run_formatter.rb +27 -0
- data/spec/fixtures/dumped_config +8 -0
- data/spec/fixtures/exchange_element/element-show.json +1 -0
- data/spec/fixtures/exchange_element/swagger-mur-6407__10k.yaml +282 -0
- data/spec/fixtures/exchange_element/swagger-mur-6407__20k.yaml +588 -0
- data/spec/variegated_TruthyFalsey_spec.rb +29 -0
- 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
|
+
|