MuranoCLI 3.0.0 → 3.0.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +50 -27
  3. data/.trustme.vim +12 -8
  4. data/bin/murano +23 -8
  5. data/docs/basic_example.rst +1 -1
  6. data/docs/completions/murano_completion-bash +88 -0
  7. data/lib/MrMurano/Account.rb +3 -3
  8. data/lib/MrMurano/Business.rb +6 -6
  9. data/lib/MrMurano/Config-Migrate.rb +1 -3
  10. data/lib/MrMurano/Config.rb +16 -8
  11. data/lib/MrMurano/Content.rb +56 -45
  12. data/lib/MrMurano/Gateway.rb +62 -21
  13. data/lib/MrMurano/Mock.rb +27 -19
  14. data/lib/MrMurano/Passwords.rb +7 -7
  15. data/lib/MrMurano/ReCommander.rb +171 -28
  16. data/lib/MrMurano/Setting.rb +38 -40
  17. data/lib/MrMurano/Solution-ServiceConfig.rb +2 -1
  18. data/lib/MrMurano/Solution-Services.rb +196 -61
  19. data/lib/MrMurano/Solution-Users.rb +7 -7
  20. data/lib/MrMurano/Solution.rb +22 -8
  21. data/lib/MrMurano/SolutionId.rb +10 -4
  22. data/lib/MrMurano/SubCmdGroupContext.rb +14 -5
  23. data/lib/MrMurano/SyncAllowed.rb +42 -0
  24. data/lib/MrMurano/SyncUpDown.rb +122 -65
  25. data/lib/MrMurano/Webservice-Cors.rb +9 -3
  26. data/lib/MrMurano/Webservice-Endpoint.rb +39 -33
  27. data/lib/MrMurano/Webservice-File.rb +35 -24
  28. data/lib/MrMurano/commands/business.rb +18 -18
  29. data/lib/MrMurano/commands/content.rb +3 -2
  30. data/lib/MrMurano/commands/devices.rb +137 -22
  31. data/lib/MrMurano/commands/globals.rb +8 -2
  32. data/lib/MrMurano/commands/keystore.rb +3 -2
  33. data/lib/MrMurano/commands/link.rb +13 -13
  34. data/lib/MrMurano/commands/login.rb +3 -2
  35. data/lib/MrMurano/commands/mock.rb +4 -3
  36. data/lib/MrMurano/commands/password.rb +4 -2
  37. data/lib/MrMurano/commands/postgresql.rb +5 -3
  38. data/lib/MrMurano/commands/settings.rb +78 -62
  39. data/lib/MrMurano/commands/show.rb +79 -74
  40. data/lib/MrMurano/commands/solution.rb +6 -4
  41. data/lib/MrMurano/commands/solution_picker.rb +5 -4
  42. data/lib/MrMurano/commands/status.rb +15 -4
  43. data/lib/MrMurano/commands/timeseries.rb +3 -2
  44. data/lib/MrMurano/commands/tsdb.rb +3 -2
  45. data/lib/MrMurano/hash.rb +6 -6
  46. data/lib/MrMurano/http.rb +66 -67
  47. data/lib/MrMurano/makePretty.rb +18 -12
  48. data/lib/MrMurano/progress.rb +9 -2
  49. data/lib/MrMurano/verbosing.rb +14 -2
  50. data/lib/MrMurano/version.rb +2 -2
  51. data/spec/GatewayDevice_spec.rb +190 -149
  52. data/spec/Mock_spec.rb +3 -3
  53. data/spec/Solution-ServiceEventHandler_spec.rb +170 -137
  54. data/spec/SyncUpDown_spec.rb +205 -191
  55. metadata +3 -2
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -32,12 +32,6 @@ module MrMurano
32
32
  return unless @path.exist?
33
33
  @path.chmod(0o600)
34
34
  @path.open('rb') do |io|
35
- # 2017-07-01: Rubocop suggests using safe_load.
36
- # https://ruby-doc.org/stdlib-2.1.0/libdoc/psych/rdoc/Psych.html#method-c-load
37
- # https://ruby-doc.org/stdlib-2.1.0/libdoc/psych/rdoc/Psych.html#method-c-safe_load
38
- # 2017-07-05: Oh, I don't think safe_load exists in Ruby 2 or 2.2...
39
- #@data = YAML.safe_load(io)
40
- # rubocop:disable Security/YAMLLoad
41
35
  @data = YAML.load(io)
42
36
  end
43
37
  end
@@ -87,6 +81,12 @@ module MrMurano
87
81
  return unless @data.is_a?(Hash)
88
82
  hd = @data[host]
89
83
  return unless !hd.nil? && hd.is_a?(Hash)
84
+ if $cfg['tool.dry']
85
+ MrMurano::Verbose.whirly_interject do
86
+ say(%(--dry: Not removing password for #{fancy_ticks("#{user}@#{host}")}))
87
+ end
88
+ return
89
+ end
90
90
  @data[host].delete(user) if hd.key?(user)
91
91
  end
92
92
 
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.16 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -7,7 +7,6 @@
7
7
 
8
8
  require 'optparse'
9
9
  require 'MrMurano/optparse'
10
-
11
10
  require 'MrMurano/verbosing'
12
11
 
13
12
  module MrMurano
@@ -50,6 +49,8 @@ module Commander
50
49
  # A command sets prompt_if_logged_off if it
51
50
  # is okay to ask the user for their password.
52
51
  attr_accessor :prompt_if_logged_off
52
+ # A command sets subcmdgrouphelp if it's help.
53
+ attr_accessor :subcmdgrouphelp
53
54
 
54
55
  def verify_arg_count!(args, max_args=0, mandatory=[])
55
56
  if !max_args.nil? && max_args.zero?
@@ -126,38 +127,180 @@ module Commander
126
127
 
127
128
  alias old_parse_global_options parse_global_options
128
129
  def parse_global_options
130
+ # User can specify, e.g., status.options, to set options for specific commands.
129
131
  defopts = ($cfg["#{active_command.name}.options"] || '').split
130
132
  @args.push(*defopts)
131
- begin
132
- old_parse_global_options
133
- rescue OptionParser::MissingArgument => err
134
- if err.message.start_with?('missing argument:')
135
- puts err.message
136
- else
137
- MrMurano::Verbose.error(
138
- "There was a problem interpreting the options: ‘#{err.message}’"
139
- )
133
+ help_hack
134
+ parse_global_options_real
135
+ end
136
+
137
+ def parse_global_options_real
138
+ old_parse_global_options
139
+ rescue OptionParser::MissingArgument => err
140
+ if err.message.start_with?('missing argument:')
141
+ puts err.message
142
+ else
143
+ err_msg = MrMurano::Verbose.fancy_ticks(err.message)
144
+ MrMurano::Verbose.error(
145
+ "There was a problem interpreting the options: #{err_msg}"
146
+ )
147
+ end
148
+ exit 2
149
+ end
150
+
151
+ # 2017-08-22: Commander's help infrastructure is either really weak,
152
+ # or we did something elsewhere that seriously cripples it. In any
153
+ # case, this fixes all its quirks.
154
+ def help_hack
155
+ do_help = fix_args
156
+ show_alias_help_maybe! if do_help
157
+ end
158
+
159
+ def help_opts
160
+ %w[-h --help help].freeze
161
+ end
162
+
163
+ def vers_opts
164
+ %w[-v --version].freeze
165
+ end
166
+
167
+ def fix_args
168
+ # If `murano --help` is specified, let rb-commander handle it.
169
+ # But if `murano command --help` is specified, don't let cmdr
170
+ # handle it, otherwise it just shows the command description,
171
+ # but not any of the subcommands (our SubCmdGroupContext code).
172
+ # Note: not checking help_opts here, which includes 'help', because
173
+ # 'help' might really be a command argument (e.g., a solution name).
174
+ # Note: `murano -v`'s active_command is 'help'.
175
+ do_help = (@args & %w[-h --help]).any? || active_command.name == 'help'
176
+ if do_help
177
+ # If there are options in addition to --help, then Commander
178
+ # runs the command! So remove all options.
179
+ #
180
+ # E.g., this shows the help for usage:
181
+ # $ murano --help usage
182
+ # but if a flag is added, the command gets run, e.g.,
183
+ # $ murano usage --id 1234 --help
184
+ # runs the usage command.
185
+ #
186
+ # I [lb] walked the code and it looks like when Commander tries to
187
+ # validate the args, it doesn't like the --id flag (maybe because
188
+ # the "help" command is being used to validate, and it does not
189
+ # define any options?). Then it removes the --id switch (after the
190
+ # --help switch was previously removed) and tries the command *again*
191
+ # (in gems/commander-4.4.3/lib/commander/runner.rb, look for the
192
+ # comment, "Remove the offending args and retry"). So in the example
193
+ # given above, `murano usage --id 1234 --help`, both the `--id` flag
194
+ # and `--help` flag are moved from @args, and then `murano usage 1234`
195
+ # is executed (and args are not validated by Commander but merely passed
196
+ # to the command action, so `usage` gets args=['1234']). Whadda wreck.
197
+ reject_next = false
198
+ @args.reject! do |arg|
199
+ reject = false
200
+ if reject_next
201
+ reject = true
202
+ reject_next = false
203
+ elsif arg.start_with?('-') && !help_opts.include?(arg) && !vers_opts.include?(arg)
204
+ reject = true
205
+ # See if the next argument should also be consumed.
206
+ arg_opts = @options.select do |opt|
207
+ if arg =~ /^-[^-]/
208
+ # Single char abbrev: look for exact match.
209
+ opt[:switches].include?(arg)
210
+ else
211
+ # Long switch: Commander matches abbrevs of longs...
212
+ opt[:switches].any? { |oarg| oarg =~ /^#{arg}/ }
213
+ end
214
+ end
215
+ if arg =~ /^--/
216
+ exact = arg_opts.select do |opt|
217
+ opt[:switches].include?(arg) || opt[:switches].any? do |sw|
218
+ sw.start_with?(arg + ' ')
219
+ end
220
+ end
221
+ arg_opts = exact unless exact.empty?
222
+ end
223
+ if arg_opts.length > 1
224
+ # MAYBE/2017-08-23: Always do this check, not just for help.
225
+ ambig = MrMurano::Verbose.fancy_ticks(arg)
226
+ match = arg_opts.map do |opt|
227
+ opt[:switches].map { |sw| MrMurano::Verbose.fancy_ticks(sw) }.join('|')
228
+ end
229
+ match = match.flatten
230
+ if match.length > 1
231
+ match[-1] = "and #{match[-1]}"
232
+ end
233
+ match = match.join(', ')
234
+ MrMurano::Verbose.error("Ambiguous option: #{ambig} matches: #{match}")
235
+ exit 2
236
+ elsif arg_opts.length == 1
237
+ if arg_opts.first[:switches].any? { |opt| opt.start_with?('--') && opt.include?(' ') }
238
+ # There's a space in the --long setting, e.g., '--config KEY=VAL',
239
+ # so we know there's an argument following.
240
+ reject_next = true
241
+ end
242
+ end
243
+ end
244
+ reject
140
245
  end
141
- exit 2
142
246
  end
247
+ @purargs = @args - help_opts
248
+ return do_help if active_command.name == 'help'
249
+ # Any command other than `murano help` or `murano --help`.
250
+ return do_help if !do_help || active_command.name.include?(' ')
251
+ # This is a single-word command, e.g., 'link', not 'link list',
252
+ # as in `murano link --help`, not `murano link list --help`.
253
+ # Positional parameters break Commander. E.g.,
254
+ # $ murano --help config application.id
255
+ # invalid command. Use --help for more information
256
+ # so remove any remaining --options and use the first term.
257
+ @args -= help_opts
258
+ @args = @args[0..0]
259
+ # Add back in the --help if the command is not a subcommand help.
260
+ @args.push('--help') unless active_command.subcmdgrouphelp
261
+ do_help
143
262
  end
144
263
 
145
- # Weird. --help doesn't work if other flags also specified.
146
- # E.g., this shows the help for usage:
147
- # $ murano --help usage
148
- # but if a flag is added, the command gets run, e.g.,
149
- # $ murano usage --id 1234 --help
150
- # ignores the --help and runs the usage command.
151
- # ([lb] walked the code and it looks like when Commander tries to
152
- # validate the args, it doesn't like the --id flag (maybe because
153
- # the "help" command is being used to validate?). Then it removes
154
- # the --id flags (after --help was previously removed) and tries
155
- # the command *again* (in gems/commander-4.4.3/lib/commander/runner.rb,
156
- # look for the comment, "Remove the offending args and retry").
157
- #
158
- # 2017-06-14: [lb] tried to override run! here to show help correctly
159
- # in this use case, but I could not get it to work. Oh, well... a
160
- # minor annoyance; just live with it, I guess.
264
+ def show_alias_help_maybe!
265
+ return unless alias?(command_name_from_args) || (active_command.name == 'help' && @args.length > 1)
266
+ # Why, oh why, Commander, do you flake out on aliases?
267
+ # E.g.,
268
+ # $ murano product --help
269
+ # invalid command. Use --help for more information
270
+ # Though it sometimes work, like with:
271
+ # $ murano --help product device enable
272
+ # but only because Commander shows help for the 'device' command.
273
+ # I.e., this doesn't work: `murano product push --help`
274
+ # So we'll just roll our own help for aliases!
275
+ @args -= help_opts
276
+ cli_cmd = MrMurano::Verbose.fancy_ticks(@purargs.join(' '))
277
+ if active_command.name == 'help'
278
+ arg_cmd = @args.join(' ')
279
+ else
280
+ arg_cmd = command_name_from_args
281
+ end
282
+ mur_msg = ''
283
+ if @aliases[arg_cmd].nil?
284
+ matches = @aliases.keys.find_all { |key| key.start_with?('arg_cmd') }
285
+ matches = @aliases.keys.find_all { |key| key.start_with?(@args[0]) } if matches.empty?
286
+ unless matches.empty?
287
+ matches = matches.map { |match| MrMurano::Verbose.fancy_ticks(match) }
288
+ matches = matches.sort.join(', ')
289
+ mur_msg = %(The #{cli_cmd} command includes: #{matches})
290
+ end
291
+ else
292
+ mur_cmd = []
293
+ mur_cmd += [active_command.name] if active_command.name != 'help'
294
+ mur_cmd += @aliases[arg_cmd] unless @aliases[arg_cmd].empty?
295
+ mur_cmd = mur_cmd.join(' ')
296
+ #mur_cmd = active_command.name if mur_cmd.empty?
297
+ mur_cmd = MrMurano::Verbose.fancy_ticks(mur_cmd)
298
+ mur_msg = %(The #{cli_cmd} command is really: #{mur_cmd})
299
+ end
300
+ return if mur_msg.empty?
301
+ puts mur_msg
302
+ exit 0
303
+ end
161
304
  end
162
305
  end
163
306
 
@@ -1,3 +1,10 @@
1
+ # Last Modified: 2017.08.20 /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
+
1
8
  require 'MrMurano/verbosing'
2
9
 
3
10
  module MrMurano
@@ -18,50 +25,41 @@ module MrMurano
18
25
  def mapservice(service)
19
26
  service = service.to_s.downcase
20
27
  SERVICE_MAP.each_pair do |k, v|
21
- if service == k.downcase || service == v.downcase
22
- return v
23
- end
28
+ return v if [k.downcase, v.downcase].include? service
24
29
  end
25
- return service.sub(/(.)(.*)/){"#{$1.upcase}#{$2.downcase}"}
30
+ # rubocop:disable Style/PerlBackrefs: "Avoid the use of Perl-style backrefs."
31
+ # Because of the upcase, we cannot just call, e.g.,
32
+ # service.sub(/(.)(.*)/, "\\1\\2")
33
+ service.sub(/(.)(.*)/) { "#{$1.upcase}#{$2.downcase}" }
26
34
  end
27
35
 
28
36
  def read(service, setting)
29
- begin
30
- debug %{Looking up class "MrMurano::#{mapservice(service)}::Settings"}
31
- gb = Object::const_get("MrMurano::#{mapservice(service)}::Settings").new
32
- meth = setting.to_sym
33
- debug %{Looking up method "#{meth}"}
34
- if gb.respond_to?(meth)
35
- return gb.__send__(meth)
36
- else
37
- error "Unknown setting '#{setting}' on '#{service}'"
38
- end
39
- rescue NameError => e
40
- error "No Settings on \"#{service}\""
41
- if $cfg['tool.debug'] then
42
- error e.message
43
- error e.to_s
44
- end
37
+ debug %(Looking up class "MrMurano::#{mapservice(service)}::Settings")
38
+ gb = Object.const_get("MrMurano::#{mapservice(service)}::Settings").new
39
+ meth = setting.to_sym
40
+ debug %(Looking up method "#{meth}")
41
+ return gb.__send__(meth) if gb.respond_to?(meth)
42
+ error "Unknown setting '#{setting}' on '#{service}'"
43
+ rescue NameError => e
44
+ error %(No Settings on "#{service}")
45
+ if $cfg['tool.debug']
46
+ error e.message
47
+ error e.to_s
45
48
  end
46
49
  end
47
50
 
48
51
  def write(service, setting, value)
49
- begin
50
- debug %(Looking up class "MrMurano::#{mapservice(service)}::Settings")
51
- gb = Object::const_get("MrMurano::#{mapservice(service)}::Settings").new
52
- meth = "#{setting}=".to_sym
53
- debug %(Looking up method "#{meth}")
54
- if gb.respond_to? meth then
55
- return gb.__send__(meth, value)
56
- else
57
- error "Unknown setting '#{setting}' on '#{service}'"
58
- end
59
- rescue NameError => e
60
- error %(No Settings on "#{service}")
61
- if $cfg['tool.debug'] then
62
- error e.message
63
- error e.to_s
64
- end
52
+ debug %(Looking up class "MrMurano::#{mapservice(service)}::Settings")
53
+ gb = Object.const_get("MrMurano::#{mapservice(service)}::Settings").new
54
+ meth = "#{setting}=".to_sym
55
+ debug %(Looking up method "#{meth}")
56
+ return gb.__send__(meth, value) if gb.respond_to? meth
57
+ error "Unknown setting '#{setting}' on '#{service}'"
58
+ rescue NameError => e
59
+ error %(No Settings on "#{service}")
60
+ if $cfg['tool.debug']
61
+ error e.message
62
+ error e.to_s
65
63
  end
66
64
  end
67
65
 
@@ -74,9 +72,11 @@ module MrMurano
74
72
  result = {}
75
73
  ::MrMurano.constants.each do |maybe|
76
74
  begin
77
- gb = Object::const_get("MrMurano::#{maybe}::Settings")
78
- result[maybe] = gb.instance_methods(false).select{|i| i.to_s[-1] != '='}
75
+ gb = Object.const_get("MrMurano::#{maybe}::Settings")
76
+ result[maybe] = gb.instance_methods(false).reject { |i| i.to_s[-1] == '=' }
77
+ # rubocop:disable Lint/HandleExceptions: Do not suppress exceptions."
79
78
  rescue
79
+ # EXPLAIN/2017-08-20: When/Why does this happen?
80
80
  end
81
81
  end
82
82
  result
@@ -86,5 +86,3 @@ module MrMurano
86
86
  end
87
87
  end
88
88
 
89
- # vim: set ai et sw=2 ts=2 :
90
-
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.17 /coding: utf-8
1
+ # Last Modified: 2017.08.22 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -70,6 +70,7 @@ module MrMurano
70
70
  end
71
71
 
72
72
  def remove(id)
73
+ return unless remove_item_allowed(id)
73
74
  delete("/#{id}")
74
75
  end
75
76
 
@@ -1,4 +1,4 @@
1
- # Last Modified: 2017.08.18 /coding: utf-8
1
+ # Last Modified: 2017.08.23 /coding: utf-8
2
2
  # frozen_string_literal: true
3
3
 
4
4
  # Copyright © 2016-2017 Exosite LLC.
@@ -54,6 +54,7 @@ module MrMurano
54
54
 
55
55
  # ??? remove
56
56
  def remove(name)
57
+ return unless remove_item_allowed(name)
57
58
  delete('/' + name)
58
59
  end
59
60
 
@@ -69,6 +70,9 @@ module MrMurano
69
70
  raise 'no file' unless thereitem.script
70
71
  script = thereitem.script
71
72
  end
73
+
74
+ script = config_vars_decode(script)
75
+
72
76
  localpath = Pathname.new(localpath) unless localpath.is_a?(Pathname)
73
77
  name = mkname(thereitem)
74
78
  pst = thereitem.to_h.merge(
@@ -79,10 +83,18 @@ module MrMurano
79
83
  name: name,
80
84
  )
81
85
  debug "f: #{localpath} >> #{pst.reject { |k, _| k == :script }.to_json}"
86
+ therealias = mkalias(thereitem)
87
+ upload_script(therealias, name, localpath, pst)
88
+ end
89
+
90
+ def upload_script(therealias, name, localpath, pst)
82
91
  # Try PUT. If 404, then POST.
83
92
  # I.e., PUT if not exists, else POST to create.
84
93
  updated_at = nil
85
- put('/' + mkalias(thereitem), pst) do |request, http|
94
+
95
+ return unless upload_item_allowed(therealias)
96
+
97
+ put('/' + therealias, pst) do |request, http|
86
98
  response = http.request(request)
87
99
  isj, jsn = isJSON(response.body)
88
100
  # ORDER: An HTTPNoContent is also a HTTPSuccess, so the latter comes first.
@@ -181,8 +193,6 @@ module MrMurano
181
193
  cache_file = $cfg.file_at(cache_file_name)
182
194
  if cache_file.file?
183
195
  cache_file.open('r+') do |io|
184
- # FIXME/2017-07-02: "Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load."
185
- # rubocop:disable Security/YAMLLoad
186
196
  cache = YAML.load(io)
187
197
  cache = {} unless cache
188
198
  io.rewind
@@ -205,7 +215,6 @@ module MrMurano
205
215
  return nil unless cache_file.file?
206
216
  ret = nil
207
217
  cache_file.open('r') do |io|
208
- # FIXME/2017-07-02: "Security/YAMLLoad: Prefer using YAML.safe_load over YAML.load."
209
218
  cache = YAML.load(io)
210
219
  return nil unless cache
211
220
  if cache.key?(local_path.to_s)
@@ -250,7 +259,6 @@ module MrMurano
250
259
  end
251
260
 
252
261
  def initialize(sid=nil)
253
- # FIXME/VERIFY/2017-07-02: Check that products do not have Modules.
254
262
  @solntype = 'application.id'
255
263
  super
256
264
  @uriparts << :module
@@ -307,14 +315,14 @@ module MrMurano
307
315
  root = root.expand_path
308
316
  if path.basename.sub(/\.lua$/i, '').to_s.include?('.')
309
317
  warning(
310
- "WARNING: Do not use periods in filenames. Rename: ‘#{path.basename}"
318
+ "WARNING: Do not use periods in filenames. Rename: #{fancy_ticks(path.basename)}"
311
319
  )
312
320
  end
313
321
  path.dirname.ascend do |ancestor|
314
322
  break if ancestor == root
315
323
  if ancestor.basename.to_s.include?('.')
316
324
  warning(
317
- "WARNING: Do not use periods in directory names. Rename: ‘#{ancestor.basename}"
325
+ "WARNING: Do not use periods in directory names. Rename: #{fancy_ticks(ancestor.basename)}"
318
326
  )
319
327
  end
320
328
  end
@@ -349,7 +357,12 @@ module MrMurano
349
357
  attr_accessor :event
350
358
  # @return [String] For device2 events, the type of event.
351
359
  attr_accessor :type
360
+ # @return [String] Service alias, e.g., "{product.id}"
361
+ attr_accessor :svc_alias
352
362
  # @return [Boolean] True if local phantom item via eventhandler.undeletable.
363
+ # I.e., the file does not exist locally and is the
364
+ # empty string on the server, so locally considered
365
+ # empty string, too.
353
366
  attr_accessor :phantom
354
367
  end
355
368
 
@@ -373,6 +386,10 @@ module MrMurano
373
386
  [remote[:service], remote[:event]].join('_')
374
387
  end
375
388
 
389
+ def synckey(item)
390
+ "#{item[:service]}_#{item[:event]}"
391
+ end
392
+
376
393
  def list(call=nil, data=nil, &block)
377
394
  ret = get(call, data, &block)
378
395
  return [] unless ret.is_a?(Hash) && !ret.key?(:error)
@@ -386,9 +403,36 @@ module MrMurano
386
403
  toss
387
404
  end
388
405
  items.map { |item| EventHandlerItem.new(item) }
389
- # MAYBE/2017-08-17:
390
- # items.map! ...
391
- # sort_by_name(items)
406
+ # MAYBE/2017-08-17:
407
+ # items.map! ...
408
+ # sort_by_name(items)
409
+ end
410
+
411
+ def resolve_config_var_usage!(there, local)
412
+ # Substitute '{product.id}' for the actual product.id if a corresponding
413
+ # local file does not exist, or if the local file already uses the alias.
414
+ encode_items = {}
415
+ local.each do |item|
416
+ encode_items[item.service] = {} if encode_items[item.service].nil?
417
+ encode_items[item.service][item.event] = item.svc_alias
418
+ end
419
+ there.map! do |item|
420
+ if (
421
+ !encode_items[item.service].nil? &&
422
+ !encode_items[item.service][item.event].nil?
423
+ )
424
+ svc_alias = encode_items[item.service][item.event]
425
+ else
426
+ # There's no corresponding local item.
427
+ _encoded, svc_alias = encode_config_var(item.service)
428
+ end
429
+ svc_alias = nil if svc_alias == item.service
430
+ unless svc_alias.nil?
431
+ item.svc_alias = svc_alias
432
+ item.name.sub!($cfg[svc_alias], "{#{svc_alias}}")
433
+ end
434
+ item
435
+ end
392
436
  end
393
437
 
394
438
  def skip?(item, skiplist)
@@ -400,15 +444,19 @@ module MrMurano
400
444
 
401
445
  def cmp_svc_evt(item, svc_evt)
402
446
  service, event = svc_evt.split('.', 2)
447
+ svc_match = (
448
+ service == item[:service] ||
449
+ (!item[:svc_alias].to_s.empty? && service == item[:svc_alias])
450
+ )
403
451
  if event.nil? || item[:event] == '*'
404
- service == item[:service]
452
+ svc_match
405
453
  else
406
454
  # rubocop:disable Style/IfInsideElse
407
455
  #svc_evt == "#{item[:service]}.#{item[:event]}"
408
456
  if service == '*'
409
457
  event == item[:event]
410
458
  else
411
- service == item[:service] && event == item[:event]
459
+ svc_match && event == item[:event]
412
460
  end
413
461
  end
414
462
  end
@@ -462,35 +510,33 @@ module MrMurano
462
510
  # @match_header finds a service and an event string, e.g., "--EVENT svc evt\n"
463
511
  md = @match_header.match(line)
464
512
  if !md.nil?
465
- # FIXME/2017-08-09: device2.event is now in the skiplist,
466
- # but some tests have a "device2 data_in" script, which
467
- # gets changed to "device2.event" here and then uploaded
468
- # (note that skiplist does not apply to local items).
469
- # You can test this code via:
470
- # rspec ./spec/cmd_syncdown_spec.rb
471
- # which has a fixture with device2.data_in specified.
472
- # QUESTION: Does writing device2.event do anything?
473
- # You cannot edit that handler from the web UI...
474
- # - Should we change the test?
475
- # - Should we get rid of this device2 hack?
476
- if md[:service] == 'device2'
477
- event_event = 'event'
478
- event_type = md[:event]
479
- # FIXME/CONFIRM/2017-07-02: 'data_in' was the old event name? It's now 'event'?
480
- # Want this?:
481
- # event_type = 'event' if event_type == 'data_in'
482
- else
483
- event_event = md[:event]
484
- event_type = nil
485
- end
486
- # header line.
513
+ service, config_var = decode_config_var(md[:service])
514
+ event_event, event_type = resolve_event_type(service, md[:event])
515
+ # Header line.
516
+ svc_alias = config_var if service != md[:service]
487
517
  cur = EventHandlerItem.new(
488
- service: md[:service],
489
- event: event_event,
490
- type: event_type,
518
+ #name
491
519
  local_path: path,
492
- line: lineno,
520
+ #id
493
521
  script: line,
522
+ line: lineno,
523
+ #line_end
524
+ #diff
525
+ #selected
526
+ #synckey
527
+ #synctype
528
+ #type
529
+ #updated_at
530
+ #dup_count
531
+ #alias
532
+ #updated_at
533
+ #created_at
534
+ #solution_id
535
+ service: service,
536
+ event: event_event,
537
+ type: event_type,
538
+ #phantom
539
+ svc_alias: svc_alias,
494
540
  )
495
541
  elsif !cur.nil? && !cur[:script].nil?
496
542
  # 2017-07-02: Frozen string literal: change << to +=
@@ -501,24 +547,27 @@ module MrMurano
501
547
  cur[:line_end] = lineno unless cur.nil?
502
548
 
503
549
  # If cur is nil here, then we need to do a :legacy check.
504
- if cur.nil? && $project['services.legacy'].is_a?(Hash)
505
- spath = path.relative_path_from(from)
506
- debug "No headers: #{spath}"
507
- service, event = $project['services.legacy'][spath.to_s]
508
- debug "Legacy lookup #{spath} => [#{service}, #{event}]"
509
- unless service.nil? || event.nil?
510
- warning %(Event in #{spath} missing header, but has legacy support.)
511
- warning %(Please add the header "--#EVENT #{service} #{event}")
512
- cur = EventHandlerItem.new(
513
- service: service,
514
- event: event,
515
- type: nil,
516
- local_path: path,
517
- line: 0,
518
- line_end: lineno,
519
- script: path.read, # FIXME: ick, fix this.
520
- )
521
- end
550
+ to_remote_item_legacy_check(cur, path, from, lineno)
551
+ end
552
+
553
+ def to_remote_item_legacy_check(cur, path, from, lineno)
554
+ return cur unless cur.nil? && $project['services.legacy'].is_a?(Hash)
555
+ spath = path.relative_path_from(from)
556
+ debug "No headers: #{spath}"
557
+ service, event = $project['services.legacy'][spath.to_s]
558
+ debug "Legacy lookup #{spath} => [#{service}, #{event}]"
559
+ unless service.nil? || event.nil?
560
+ warning %(Event in #{spath} missing header, but has legacy support.)
561
+ warning %(Please add the header "--#EVENT #{service} #{event}")
562
+ cur = EventHandlerItem.new(
563
+ service: service,
564
+ event: event,
565
+ type: nil,
566
+ local_path: path,
567
+ line: 0,
568
+ line_end: lineno,
569
+ script: path.read, # FIXME: ick, fix this.
570
+ )
522
571
  end
523
572
  cur
524
573
  end
@@ -529,13 +578,99 @@ module MrMurano
529
578
  md = pattern_pattern.match(pattern)
530
579
  return false if md.nil?
531
580
  debug "match pattern: '#{md[:service]}' '#{md[:event]}'"
532
- return false unless md[:service].empty? || item[:service].casecmp(md[:service]).zero?
581
+ if (
582
+ !md[:service].empty? &&
583
+ !item[:service].casecmp(md[:service]).zero? &&
584
+ (item[:svc_alias].to_s.empty? || !item[:svc_alias].casecmp(md[:service]).zero?)
585
+ )
586
+ return false
587
+ end
533
588
  return false unless md[:event].empty? || item[:event].casecmp(md[:event]).zero?
534
- true # Both match (or are empty.)
589
+ true # Both match (or both empty).
535
590
  end
536
591
 
537
- def synckey(item)
538
- "#{item[:service]}_#{item[:event]}"
592
+ def config_vars_decode(script)
593
+ config_vars_translate(script, method(:decode_config_var))
594
+ end
595
+
596
+ def config_vars_encode(script)
597
+ config_vars_translate(script, method(:encode_config_var))
598
+ end
599
+
600
+ def config_vars_translate(script, codec)
601
+ new_script = ''
602
+ # Replace the service with a {config.variable} if appropriate.
603
+ script.lines.each do |line|
604
+ # @match_header finds a service and an event string, e.g., "--EVENT svc evt\n"
605
+ md = @match_header.match(line)
606
+ unless md.nil?
607
+ service, _config_var = codec.call(md[:service])
608
+ line = line.gsub(
609
+ /--#EVENT (?<service>\S+) (?<event>\S+)/,
610
+ "--#EVENT #{service} \\k<event>"
611
+ )
612
+ end
613
+ new_script += line
614
+ end
615
+ new_script
616
+ end
617
+
618
+ def decode_config_var(term)
619
+ decoded = term
620
+ config_var = nil
621
+ # Try to match svc against a {config.variable}.
622
+ mat = /^\{([^\.}]+)\.([^\.}]+)\}$/.match(term)
623
+ unless mat.nil?
624
+ # User specified a config value, e.g., "--#EVENT {product.id} event".
625
+ config_var = "#{mat[1]}.#{mat[2]}"
626
+ decoded = $cfg[config_var]
627
+ if decoded.nil?
628
+ warning "Config value not found for variable: #{fancy_ticks(term)}"
629
+ # The platform should complain about file not found,
630
+ # and we'll process everything else.
631
+ decoded = term
632
+ config_var = nil
633
+ end
634
+ end
635
+ [decoded, config_var]
636
+ end
637
+
638
+ def encode_config_var(term)
639
+ encoded = term
640
+ config_var = nil
641
+ # MEH/2017-08-22: For now, only matching against one config var.
642
+ # Maybe in the future we'd want to examine all the config vars?
643
+ # MAGIC_STRING: {config.variable} substitution.
644
+ if term == $cfg['product.id']
645
+ encoded = '{product.id}'
646
+ config_var = 'product.id'
647
+ end
648
+ [encoded, config_var]
649
+ end
650
+
651
+ def resolve_event_type(service, event)
652
+ # FIXME/2017-08-09: device2.event is now in the skiplist,
653
+ # but some tests have a "device2 data_in" script, which
654
+ # gets changed to "device2.event" here and then uploaded
655
+ # (note that skiplist does not apply to local items).
656
+ # You can test this code via:
657
+ # rspec ./spec/cmd_syncdown_spec.rb
658
+ # which has a fixture with device2.data_in specified.
659
+ # QUESTION: Does writing device2.event do anything?
660
+ # You cannot edit that handler from the web UI...
661
+ # - Should we change the test?
662
+ # - Should we get rid of this device2 hack?
663
+ if service == 'device2'
664
+ event_event = 'event'
665
+ event_type = event
666
+ # FIXME/CONFIRM/2017-07-02: 'data_in' was the old event name? It's now 'event'?
667
+ # Want this?:
668
+ # event_type = 'event' if event_type == 'data_in'
669
+ else
670
+ event_event = event
671
+ event_type = nil
672
+ end
673
+ [event_event, event_type]
539
674
  end
540
675
 
541
676
  def resurrect_undeletables(localbox, therebox)