MrMurano 1.11.3 → 1.12.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/Gemfile +2 -0
  4. data/README.markdown +12 -14
  5. data/Rakefile +2 -2
  6. data/TODO.taskpaper +8 -6
  7. data/docs/demo.md +109 -0
  8. data/lib/MrMurano/Account.rb +6 -3
  9. data/lib/MrMurano/Config.rb +13 -37
  10. data/lib/MrMurano/Product-1P-Device.rb +2 -0
  11. data/lib/MrMurano/Product-Resources.rb +5 -4
  12. data/lib/MrMurano/Product.rb +3 -3
  13. data/lib/MrMurano/Solution-File.rb +1 -2
  14. data/lib/MrMurano/Solution-ServiceConfig.rb +11 -1
  15. data/lib/MrMurano/Solution-Services.rb +24 -7
  16. data/lib/MrMurano/Solution-Users.rb +6 -4
  17. data/lib/MrMurano/SyncUpDown.rb +67 -28
  18. data/lib/MrMurano/commands/config.rb +4 -1
  19. data/lib/MrMurano/commands/exportImport.rb +3 -0
  20. data/lib/MrMurano/hash.rb +2 -0
  21. data/lib/MrMurano/http.rb +3 -3
  22. data/lib/MrMurano/verbosing.rb +9 -3
  23. data/lib/MrMurano/version.rb +1 -1
  24. data/spec/Account_spec.rb +104 -0
  25. data/spec/Config_spec.rb +202 -57
  26. data/spec/Http_spec.rb +204 -0
  27. data/spec/MakePretties_spec.rb +35 -0
  28. data/spec/Mock_spec.rb +13 -20
  29. data/spec/ProductBase_spec.rb +2 -0
  30. data/spec/ProductContent_spec.rb +77 -12
  31. data/spec/ProductResources_spec.rb +235 -0
  32. data/spec/Product_1P_Device_spec.rb +62 -0
  33. data/spec/Product_1P_RPC_spec.rb +2 -0
  34. data/spec/Product_spec.rb +18 -8
  35. data/spec/Solution-Cors_spec.rb +28 -1
  36. data/spec/Solution-Endpoint_spec.rb +250 -33
  37. data/spec/Solution-File_spec.rb +210 -0
  38. data/spec/Solution-ServiceConfig_spec.rb +2 -0
  39. data/spec/Solution-ServiceDevice_spec.rb +174 -0
  40. data/spec/Solution-ServiceEventHandler_spec.rb +134 -1
  41. data/spec/Solution-ServiceModules_spec.rb +317 -64
  42. data/spec/Solution-UsersRoles_spec.rb +207 -0
  43. data/spec/Solution_spec.rb +90 -0
  44. data/spec/SyncRoot_spec.rb +52 -46
  45. data/spec/SyncUpDown_spec.rb +364 -0
  46. data/spec/Verbosing_spec.rb +279 -0
  47. data/spec/_workspace.rb +27 -0
  48. data/spec/fixtures/dumped_config +47 -0
  49. data/spec/fixtures/product_spec_files/lightbulb-no-state.yaml +11 -0
  50. data/spec/fixtures/product_spec_files/lightbulb.yaml +2 -0
  51. data/spec/fixtures/roles-three.yaml +11 -0
  52. data/spec/spec_helper.rb +9 -0
  53. metadata +26 -2
@@ -1,6 +1,7 @@
1
1
  require 'pathname'
2
2
  require 'tempfile'
3
3
  require 'shellwords'
4
+ require 'open3'
4
5
  require 'MrMurano/Config'
5
6
  require 'MrMurano/hash'
6
7
 
@@ -10,23 +11,52 @@ module MrMurano
10
11
  Syncable = Struct.new(:name, :class, :type, :desc, :bydefault) do
11
12
  end
12
13
 
14
+ ##
15
+ # Add a new entry to syncable things
16
+ # +name+:: The name to use for the long option
17
+ # +klass+:: The class to instanciate from
18
+ # +type+:: Single letter for short option and status listing
19
+ # +desc+:: Summary of what this syncs.
20
+ # +bydefault+:: Is this part of the default sync group
21
+ #
22
+ # returns nil
13
23
  def self.add(name, klass, type, desc, bydefault=false)
14
24
  @@syncset = [] unless defined?(@@syncset)
15
25
  @@syncset << Syncable.new(name.to_s, klass, type, desc, bydefault)
26
+ nil
16
27
  end
17
28
 
29
+ ##
30
+ # Remove all syncables.
18
31
  def self.reset()
19
32
  @@syncset = []
20
33
  end
21
34
 
35
+ ##
36
+ # Get the list of default syncables.
37
+ # returns array of names
38
+ def self.bydefault
39
+ @@syncset.select{|a| a.bydefault }.map{|a| a.name}
40
+ end
41
+
42
+ ##
43
+ # Iterate over all syncables
44
+ # +block+:: code to run on each
22
45
  def self.each(&block)
23
46
  @@syncset.each{|a| yield a.name, a.type, a.class }
24
47
  end
25
48
 
49
+ ##
50
+ # Iterate over all syncables with option arguments.
51
+ # +block+:: code to run on each
26
52
  def self.each_option(&block)
27
53
  @@syncset.each{|a| yield "-#{a.type.downcase}", "--[no-]#{a.name}", a.desc}
28
54
  end
29
55
 
56
+ ##
57
+ # Iterate over just the selected syncables.
58
+ # +opt+:: Options hash of which to select from
59
+ # +block+:: code to run on each
30
60
  def self.each_filtered(opt, &block)
31
61
  self.checkSAME(opt)
32
62
  @@syncset.each do |a|
@@ -38,13 +68,18 @@ module MrMurano
38
68
 
39
69
  ## Adjust options based on all or none
40
70
  # If none are selected, select the bydefault ones.
71
+ #
72
+ # +opt+:: Options hash of which to select from
73
+ #
74
+ # returns nil
41
75
  def self.checkSAME(opt)
42
76
  if opt[:all] then
43
77
  @@syncset.each {|a| opt[a.name.to_sym] = true }
44
78
  else
45
79
  any = @@syncset.select {|a| opt[a.name.to_sym] or opt[a.type.to_sym]}
46
80
  if any.empty? then
47
- @@syncset.select{|a| a.bydefault }.each{|a| opt[a.name.to_sym] = true}
81
+ bydef = $cfg['sync.bydefault'].split
82
+ @@syncset.select{|a| bydef.include? a.name }.each{|a| opt[a.name.to_sym] = true}
48
83
  end
49
84
  end
50
85
 
@@ -72,7 +107,9 @@ module MrMurano
72
107
  #
73
108
  # @param itemkey String: The identifying key for this item
74
109
  def remove(itemkey)
110
+ # :nocov:
75
111
  raise "Forgotten implementation"
112
+ # :nocov:
76
113
  end
77
114
 
78
115
  ## Upload local item to remote
@@ -83,7 +120,9 @@ module MrMurano
83
120
  # @param item Hash: The item details to upload
84
121
  # @param modify Bool: True if item exists already and this is changing it
85
122
  def upload(src, item, modify)
123
+ # :nocov:
86
124
  raise "Forgotten implementation"
125
+ # :nocov:
87
126
  end
88
127
 
89
128
  ##
@@ -112,7 +151,7 @@ module MrMurano
112
151
  def toRemoteItem(root, path)
113
152
  path = Pathname.new(path) unless path.kind_of? Pathname
114
153
  root = Pathname.new(root) unless root.kind_of? Pathname
115
- {:name => path.relative_path_from(root).to_s}
154
+ {:name => path.realpath.relative_path_from(root.realpath).to_s}
116
155
  end
117
156
 
118
157
  ##
@@ -166,8 +205,7 @@ module MrMurano
166
205
  # @param item Hash: The item to download
167
206
  def download(local, item)
168
207
  if item[:bundled] then
169
- say_warning "Not downloading into bundled item #{synckey(item)}"
170
- # FIXME don't use say_warning
208
+ warning "Not downloading into bundled item #{synckey(item)}"
171
209
  return
172
210
  end
173
211
  local.dirname.mkpath
@@ -275,7 +313,7 @@ module MrMurano
275
313
  ::File.fnmatch(i,p)
276
314
  end
277
315
  end.map do |path|
278
- path = Pathname.new(path)
316
+ path = Pathname.new(path).realpath
279
317
  item = toRemoteItem(from, path)
280
318
  if item.kind_of?(Array) then
281
319
  item.compact.map{|i| i[:local_path] = path; i}
@@ -289,31 +327,32 @@ module MrMurano
289
327
  #######################################################################
290
328
  # Methods that provide the core status/syncup/syncdown
291
329
 
330
+ ##
331
+ # Take a hash or something (a Commander::Command::Options) and return a hash
332
+ #
292
333
  def elevate_hash(hsh)
293
- if hsh.kind_of?(Hash) then
294
- hsh = Hash.transform_keys_to_symbols(hsh)
295
- hsh.define_singleton_method(:method_missing) do |mid,*args|
296
- if mid.to_s.match(/^(.+)=$/) then
297
- self[$1.to_sym] = args.first
298
- else
299
- self[mid]
300
- end
301
- end
334
+ # Commander::Command::Options stripped all of the methods from parent
335
+ # objects. I have not nice thoughts about that.
336
+ begin
337
+ hsh = hsh.__hash__
338
+ rescue NoMethodError
339
+ # swallow this.
302
340
  end
303
- hsh
341
+ # build a hash where the default is 'false' instead of 'nil'
342
+ Hash.new(false).merge(Hash.transform_keys_to_symbols(hsh))
304
343
  end
305
344
  private :elevate_hash
306
345
 
307
346
  def syncup(options={})
308
347
  options = elevate_hash(options)
309
348
  itemkey = @itemkey.to_sym
310
- options.asdown=false
349
+ options[:asdown] = false
311
350
  dt = status(options)
312
351
  toadd = dt[:toadd]
313
352
  todel = dt[:todel]
314
353
  tomod = dt[:tomod]
315
354
 
316
- if options.delete then
355
+ if options[:delete] then
317
356
  todel.each do |item|
318
357
  verbose "Removing item #{item[:synckey]}"
319
358
  unless $cfg['tool.dry'] then
@@ -321,7 +360,7 @@ module MrMurano
321
360
  end
322
361
  end
323
362
  end
324
- if options.create then
363
+ if options[:create] then
325
364
  toadd.each do |item|
326
365
  verbose "Adding item #{item[:synckey]}"
327
366
  unless $cfg['tool.dry'] then
@@ -329,7 +368,7 @@ module MrMurano
329
368
  end
330
369
  end
331
370
  end
332
- if options.update then
371
+ if options[:update] then
333
372
  tomod.each do |item|
334
373
  verbose "Updating item #{item[:synckey]}"
335
374
  unless $cfg['tool.dry'] then
@@ -341,14 +380,14 @@ module MrMurano
341
380
 
342
381
  def syncdown(options={})
343
382
  options = elevate_hash(options)
344
- options.asdown = true
383
+ options[:asdown] = true
345
384
  dt = status(options)
346
385
  into = @locationbase + @location ###
347
386
  toadd = dt[:toadd]
348
387
  todel = dt[:todel]
349
388
  tomod = dt[:tomod]
350
389
 
351
- if options.delete then
390
+ if options[:delete] then
352
391
  todel.each do |item|
353
392
  verbose "Removing item #{item[:synckey]}"
354
393
  unless $cfg['tool.dry'] then
@@ -357,7 +396,7 @@ module MrMurano
357
396
  end
358
397
  end
359
398
  end
360
- if options.create then
399
+ if options[:create] then
361
400
  toadd.each do |item|
362
401
  verbose "Adding item #{item[:synckey]}"
363
402
  unless $cfg['tool.dry'] then
@@ -366,7 +405,7 @@ module MrMurano
366
405
  end
367
406
  end
368
407
  end
369
- if options.update then
408
+ if options[:update] then
370
409
  tomod.each do |item|
371
410
  verbose "Updating item #{item[:synckey]}"
372
411
  unless $cfg['tool.dry'] then
@@ -398,10 +437,10 @@ module MrMurano
398
437
  download(Pathname.new(trmt.path), item)
399
438
 
400
439
  cmd = $cfg['diff.cmd'].shellsplit
401
- cmd << trmt.path
402
- cmd << tlcl.path
440
+ cmd << trmt.path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR)
441
+ cmd << tlcl.path.gsub(::File::SEPARATOR, ::File::ALT_SEPARATOR || ::File::SEPARATOR)
403
442
 
404
- IO.popen(cmd) {|io| df = io.read }
443
+ df, _ = Open3.capture2e(*cmd)
405
444
  ensure
406
445
  trmt.close
407
446
  trmt.unlink
@@ -434,7 +473,7 @@ module MrMurano
434
473
  todel = []
435
474
  tomod = []
436
475
  unchg = []
437
- if options.asdown then
476
+ if options[:asdown] then
438
477
  todel = (herebox.keys - therebox.keys).map{|key| herebox[key] }
439
478
  toadd = (therebox.keys - herebox.keys).map{|key| therebox[key] }
440
479
  else
@@ -446,7 +485,7 @@ module MrMurano
446
485
  mrg = herebox[key].reject{|k,v| k==itemkey}
447
486
  mrg = therebox[key].merge(mrg)
448
487
  if docmp(herebox[key], therebox[key]) then
449
- mrg[:diff] = dodiff(mrg) if options.diff
488
+ mrg[:diff] = dodiff(mrg) if options[:diff]
450
489
  tomod << mrg
451
490
  else
452
491
  unchg << mrg
@@ -6,13 +6,16 @@ command :config do |c|
6
6
  You can get, set, or query config options with this command. All config
7
7
  options are in a 'section.key' format. There is also a layer of scopes
8
8
  that the keys can be saved in.
9
+
10
+ If section is left out, then key is assumed to be in the 'tool' section.
11
+
9
12
  }
10
13
 
11
14
  c.example %{See what the current combined config is}, 'mr config --dump'
12
15
  c.example %{Query a value}, 'mr config solution.id'
13
16
  c.example %{Set a new value; writing to the project config file}, 'mr config solution.id XXXXXXXX'
14
17
  c.example %{Set a new value; writing to the user config file}, 'mr config --user user.name my@email.address'
15
- c.example %{Unset a value in a configfile. (lower scopes will become visible if set)},
18
+ c.example %{Unset a value in a configfile. (lower scopes will become visible when unset)},
16
19
  'mr config diff.cmd --unset'
17
20
 
18
21
 
@@ -119,6 +119,8 @@ command 'config import' do |c|
119
119
  if routes == '' then
120
120
  acc.verbose "No endpoints to import"
121
121
  elsif File.dirname(routes) == '.' then
122
+ # TODO: don't need to move this anymore.
123
+ # Can set location.endpoints and endpoints.searchFor instead.
122
124
  acc.warning "Routes file #{File.basename(routes)} not in endpoints directory"
123
125
  if options.move then
124
126
  acc.warning "Moving it to #{$cfg['location.endpoints']}"
@@ -142,6 +144,7 @@ command 'config import' do |c|
142
144
  end
143
145
  end
144
146
 
147
+ # TODO: change this to take advantage of searchFor
145
148
  def update_or_stop(paths, cfgkey, what, acc=MrMurano::Account.new)
146
149
  crd = Dir.common_root(paths)
147
150
  acc.debug "crd => #{crd}"
data/lib/MrMurano/hash.rb CHANGED
@@ -3,12 +3,14 @@
3
3
  class Hash
4
4
  #take keys of hash and transform those to a symbols
5
5
  def self.transform_keys_to_symbols(value)
6
+ return value.map{|v| Hash.transform_keys_to_symbols(v)} if value.is_a?(Array)
6
7
  return value if not value.is_a?(Hash)
7
8
  hash = value.inject({}){|memo,(k,v)| memo[k.to_sym] = Hash.transform_keys_to_symbols(v); memo}
8
9
  return hash
9
10
  end
10
11
  #take keys of hash and transform those to strings
11
12
  def self.transform_keys_to_strings(value)
13
+ return value.map{|v| Hash.transform_keys_to_strings(v)} if value.is_a?(Array)
12
14
  return value if not value.is_a?(Hash)
13
15
  hash = value.inject({}){|memo,(k,v)| memo[k.to_s] = Hash.transform_keys_to_strings(v); memo}
14
16
  return hash
data/lib/MrMurano/http.rb CHANGED
@@ -70,7 +70,7 @@ module MrMurano
70
70
  request.each_capitalized{|k,v| puts "> #{k}: #{v}"}
71
71
  if request.body.nil? then
72
72
  else
73
- puts " > #{request.body[0..156]}"
73
+ puts ">> #{request.body[0..156]}"
74
74
  end
75
75
  puts "Got #{response.code} #{response.message}"
76
76
  response.each_capitalized{|k,v| puts "< #{k}: #{v}"}
@@ -87,7 +87,8 @@ module MrMurano
87
87
  else
88
88
  resp << jsn
89
89
  end
90
- say_error resp
90
+ # assuming verbosing was included.
91
+ error resp
91
92
  end
92
93
 
93
94
  def workit(request, &block)
@@ -106,7 +107,6 @@ module MrMurano
106
107
  end
107
108
  else
108
109
  showHttpError(request, response)
109
- raise response
110
110
  end
111
111
  end
112
112
  end
@@ -34,8 +34,8 @@ module MrMurano
34
34
  def tabularize(data, ios=nil)
35
35
  fmt = $cfg['tool.outformat']
36
36
  ios = $stdout if ios.nil?
37
- cols = []
38
- rows = [[]]
37
+ cols = nil
38
+ rows = nil
39
39
  title = nil
40
40
  if data.kind_of?(Hash) then
41
41
  cols = data[:headers] if data.has_key?(:headers)
@@ -53,12 +53,18 @@ module MrMurano
53
53
  return
54
54
  end
55
55
  if fmt =~ /csv/i then
56
+ cols = [] if cols.nil?
57
+ rows = [[]] if rows.nil?
56
58
  CSV(ios, :headers=>cols, :write_headers=>(not cols.empty?)) do |csv|
57
59
  rows.each{|v| csv << v}
58
60
  end
59
61
  else
60
62
  # table.
61
- ios.puts Terminal::Table.new :title=>title, :headings=>cols, :rows=>rows
63
+ table = Terminal::Table.new
64
+ table.title = title unless title.nil?
65
+ table.headings = cols unless cols.nil?
66
+ table.rows = rows unless rows.nil?
67
+ ios.puts table
62
68
  end
63
69
  end
64
70
 
@@ -1,4 +1,4 @@
1
1
  module MrMurano
2
- VERSION = '1.11.3'.freeze
2
+ VERSION = '1.12.0'.freeze
3
3
  end
4
4
 
data/spec/Account_spec.rb CHANGED
@@ -1,8 +1,112 @@
1
1
  require 'MrMurano/version'
2
2
  require 'MrMurano/Config'
3
3
  require 'MrMurano/Account'
4
+ require 'highline/import'
5
+ require '_workspace'
6
+
7
+ RSpec.describe MrMurano::Account, "token" do
8
+ include_context "WORKSPACE"
9
+ before(:example) do
10
+ $cfg = MrMurano::Config.new
11
+ $cfg.load
12
+ $cfg['net.host'] = 'bizapi.hosted.exosite.io'
13
+ $cfg['business.id'] = 'XYZxyz'
14
+ $cfg['product.id'] = 'XYZ'
15
+
16
+ @acc = MrMurano::Account.new
17
+ end
18
+
19
+ after(:example) do
20
+ @acc.token_reset
21
+ end
22
+
23
+ context "Get login info" do
24
+ before(:example) do
25
+ @pswd = instance_double("MrMurano::Passwords")
26
+ allow(@pswd).to receive(:load).and_return(nil)
27
+ allow(@pswd).to receive(:save).and_return(nil)
28
+ allow(MrMurano::Passwords).to receive(:new).and_return(@pswd)
29
+ end
30
+
31
+ it "Asks for nothing" do
32
+ $cfg['user.name'] = "bob"
33
+ expect(@pswd).to receive(:get).once.and_return("built")
34
+
35
+ ret = @acc._loginInfo
36
+ expect(ret).to eq({
37
+ :email => "bob", :password=>"built"
38
+ })
39
+ end
40
+
41
+ it "Asks for user name" do
42
+ $cfg['user.name'] = nil
43
+ expect($terminal).to receive(:ask).once.and_return('bob')
44
+ expect(@acc).to receive(:error).once
45
+ expect($cfg).to receive(:set).with('user.name', 'bob', :user).once.and_call_original
46
+ expect(@pswd).to receive(:get).once.and_return("built")
47
+
48
+ ret = @acc._loginInfo
49
+ expect(ret).to eq({
50
+ :email => "bob", :password=>"built"
51
+ })
52
+ end
53
+
54
+ it "Asks for password" do
55
+ $cfg['user.name'] = "bob"
56
+ expect(@pswd).to receive(:get).with('bizapi.hosted.exosite.io','bob').once.and_return(nil)
57
+ expect(@acc).to receive(:error).once
58
+ expect($terminal).to receive(:ask).once.and_return('dog')
59
+ expect(@pswd).to receive(:set).once.with('bizapi.hosted.exosite.io','bob','dog')
60
+
61
+ ret = @acc._loginInfo
62
+ expect(ret).to eq({
63
+ :email => "bob", :password=>"dog"
64
+ })
65
+ end
66
+ end
67
+
68
+ context "token" do
69
+ before(:example) do
70
+ allow(@acc).to receive(:_loginInfo).and_return({:email=>'bob',:password=>'v'})
71
+ end
72
+
73
+ it "gets a token" do
74
+ stub_request(:post, "https://bizapi.hosted.exosite.io/api:1/token/").
75
+ with(:body => {:email=>'bob', :password=>'v'}.to_json).
76
+ to_return(body: {:token=>"ABCDEFGHIJKLMNOP"}.to_json )
77
+
78
+ ret = @acc.token
79
+ expect(ret).to eq("ABCDEFGHIJKLMNOP")
80
+ end
81
+
82
+ it "gets an error" do
83
+ stub_request(:post, "https://bizapi.hosted.exosite.io/api:1/token/").
84
+ with(:body => {:email=>'bob', :password=>'v'}.to_json).
85
+ to_return(status: 401, body: {}.to_json )
86
+
87
+ expect(@acc).to receive(:error).twice.and_return(nil)
88
+ ret = @acc.token
89
+ expect(ret).to be_nil
90
+ end
91
+
92
+ it "uses existing token" do
93
+ @acc.token_reset("quxx")
94
+ ret = @acc.token
95
+ expect(ret).to eq("quxx")
96
+ end
97
+
98
+ it "uses existing token, even with new instance" do
99
+ @acc.token_reset("quxx")
100
+ acc = MrMurano::Account.new
101
+ ret = acc.token
102
+ expect(ret).to eq("quxx")
103
+ end
104
+
105
+ end
106
+ end
4
107
 
5
108
  RSpec.describe MrMurano::Account do
109
+ include_context "WORKSPACE"
6
110
  before(:example) do
7
111
  $cfg = MrMurano::Config.new
8
112
  $cfg.load