MuranoCLI 2.0.0

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 (151) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +28 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +21 -0
  5. data/Gemfile +27 -0
  6. data/LICENSE.txt +19 -0
  7. data/MuranoCLI.gemspec +50 -0
  8. data/MuranoCLI.iss +50 -0
  9. data/README.markdown +208 -0
  10. data/Rakefile +188 -0
  11. data/TODO.taskpaper +122 -0
  12. data/bin/mr +8 -0
  13. data/bin/murano +84 -0
  14. data/docs/demo.md +109 -0
  15. data/lib/MrMurano/Account.rb +211 -0
  16. data/lib/MrMurano/Config-Migrate.rb +47 -0
  17. data/lib/MrMurano/Config.rb +286 -0
  18. data/lib/MrMurano/Mock.rb +63 -0
  19. data/lib/MrMurano/Product-1P-Device.rb +145 -0
  20. data/lib/MrMurano/Product-Resources.rb +195 -0
  21. data/lib/MrMurano/Product.rb +358 -0
  22. data/lib/MrMurano/ProjectFile.rb +349 -0
  23. data/lib/MrMurano/Solution-Cors.rb +46 -0
  24. data/lib/MrMurano/Solution-Endpoint.rb +177 -0
  25. data/lib/MrMurano/Solution-File.rb +150 -0
  26. data/lib/MrMurano/Solution-ServiceConfig.rb +140 -0
  27. data/lib/MrMurano/Solution-Services.rb +326 -0
  28. data/lib/MrMurano/Solution-Users.rb +129 -0
  29. data/lib/MrMurano/Solution.rb +59 -0
  30. data/lib/MrMurano/SubCmdGroupContext.rb +49 -0
  31. data/lib/MrMurano/SyncUpDown.rb +565 -0
  32. data/lib/MrMurano/commands/assign.rb +57 -0
  33. data/lib/MrMurano/commands/businessList.rb +45 -0
  34. data/lib/MrMurano/commands/completion.rb +152 -0
  35. data/lib/MrMurano/commands/config.rb +67 -0
  36. data/lib/MrMurano/commands/content.rb +130 -0
  37. data/lib/MrMurano/commands/cors.rb +30 -0
  38. data/lib/MrMurano/commands/domain.rb +17 -0
  39. data/lib/MrMurano/commands/gb.rb +33 -0
  40. data/lib/MrMurano/commands/init.rb +138 -0
  41. data/lib/MrMurano/commands/keystore.rb +157 -0
  42. data/lib/MrMurano/commands/logs.rb +78 -0
  43. data/lib/MrMurano/commands/mock.rb +63 -0
  44. data/lib/MrMurano/commands/password.rb +88 -0
  45. data/lib/MrMurano/commands/postgresql.rb +41 -0
  46. data/lib/MrMurano/commands/product.rb +14 -0
  47. data/lib/MrMurano/commands/productCreate.rb +39 -0
  48. data/lib/MrMurano/commands/productDelete.rb +33 -0
  49. data/lib/MrMurano/commands/productDevice.rb +84 -0
  50. data/lib/MrMurano/commands/productDeviceIdCmds.rb +86 -0
  51. data/lib/MrMurano/commands/productList.rb +45 -0
  52. data/lib/MrMurano/commands/productWrite.rb +27 -0
  53. data/lib/MrMurano/commands/show.rb +80 -0
  54. data/lib/MrMurano/commands/solution.rb +14 -0
  55. data/lib/MrMurano/commands/solutionCreate.rb +39 -0
  56. data/lib/MrMurano/commands/solutionDelete.rb +34 -0
  57. data/lib/MrMurano/commands/solutionList.rb +45 -0
  58. data/lib/MrMurano/commands/status.rb +92 -0
  59. data/lib/MrMurano/commands/sync.rb +60 -0
  60. data/lib/MrMurano/commands/timeseries.rb +115 -0
  61. data/lib/MrMurano/commands/tsdb.rb +271 -0
  62. data/lib/MrMurano/commands/usage.rb +23 -0
  63. data/lib/MrMurano/commands/zshcomplete.erb +112 -0
  64. data/lib/MrMurano/commands.rb +32 -0
  65. data/lib/MrMurano/hash.rb +20 -0
  66. data/lib/MrMurano/http.rb +153 -0
  67. data/lib/MrMurano/makePretty.rb +75 -0
  68. data/lib/MrMurano/schema/pf-v1.0.0.yaml +114 -0
  69. data/lib/MrMurano/schema/sf-v0.2.0.yaml +77 -0
  70. data/lib/MrMurano/schema/sf-v0.3.0.yaml +78 -0
  71. data/lib/MrMurano/template/mock.erb +9 -0
  72. data/lib/MrMurano/template/projectFile.murano.erb +81 -0
  73. data/lib/MrMurano/verbosing.rb +99 -0
  74. data/lib/MrMurano/version.rb +4 -0
  75. data/lib/MrMurano.rb +20 -0
  76. data/spec/Account-Passwords_spec.rb +242 -0
  77. data/spec/Account_spec.rb +272 -0
  78. data/spec/ConfigFile_spec.rb +50 -0
  79. data/spec/ConfigMigrate_spec.rb +89 -0
  80. data/spec/Config_spec.rb +409 -0
  81. data/spec/Http_spec.rb +204 -0
  82. data/spec/MakePretties_spec.rb +118 -0
  83. data/spec/Mock_spec.rb +53 -0
  84. data/spec/ProductBase_spec.rb +113 -0
  85. data/spec/ProductContent_spec.rb +162 -0
  86. data/spec/ProductResources_spec.rb +329 -0
  87. data/spec/Product_1P_Device_spec.rb +202 -0
  88. data/spec/Product_1P_RPC_spec.rb +175 -0
  89. data/spec/Product_spec.rb +153 -0
  90. data/spec/ProjectFile_spec.rb +324 -0
  91. data/spec/Solution-Cors_spec.rb +164 -0
  92. data/spec/Solution-Endpoint_spec.rb +581 -0
  93. data/spec/Solution-File_spec.rb +212 -0
  94. data/spec/Solution-ServiceConfig_spec.rb +202 -0
  95. data/spec/Solution-ServiceDevice_spec.rb +176 -0
  96. data/spec/Solution-ServiceEventHandler_spec.rb +385 -0
  97. data/spec/Solution-ServiceModules_spec.rb +465 -0
  98. data/spec/Solution-UsersRoles_spec.rb +207 -0
  99. data/spec/Solution_spec.rb +92 -0
  100. data/spec/SyncRoot_spec.rb +83 -0
  101. data/spec/SyncUpDown_spec.rb +495 -0
  102. data/spec/Verbosing_spec.rb +279 -0
  103. data/spec/_workspace.rb +27 -0
  104. data/spec/cmd_assign_spec.rb +51 -0
  105. data/spec/cmd_business_spec.rb +59 -0
  106. data/spec/cmd_common.rb +72 -0
  107. data/spec/cmd_config_spec.rb +68 -0
  108. data/spec/cmd_content_spec.rb +71 -0
  109. data/spec/cmd_cors_spec.rb +50 -0
  110. data/spec/cmd_device_spec.rb +96 -0
  111. data/spec/cmd_domain_spec.rb +32 -0
  112. data/spec/cmd_init_spec.rb +30 -0
  113. data/spec/cmd_keystore_spec.rb +97 -0
  114. data/spec/cmd_password_spec.rb +62 -0
  115. data/spec/cmd_status_spec.rb +239 -0
  116. data/spec/cmd_syncdown_spec.rb +86 -0
  117. data/spec/cmd_syncup_spec.rb +62 -0
  118. data/spec/cmd_usage_spec.rb +36 -0
  119. data/spec/fixtures/.mrmuranorc +9 -0
  120. data/spec/fixtures/ProjectFiles/invalid.yaml +9 -0
  121. data/spec/fixtures/ProjectFiles/only_meta.yaml +24 -0
  122. data/spec/fixtures/ProjectFiles/with_routes.yaml +27 -0
  123. data/spec/fixtures/SolutionFiles/0.2.0.json +20 -0
  124. data/spec/fixtures/SolutionFiles/0.2.0_invalid.json +18 -0
  125. data/spec/fixtures/SolutionFiles/0.2.json +21 -0
  126. data/spec/fixtures/SolutionFiles/0.3.0.json +20 -0
  127. data/spec/fixtures/SolutionFiles/0.3.0_invalid.json +19 -0
  128. data/spec/fixtures/SolutionFiles/0.3.json +20 -0
  129. data/spec/fixtures/SolutionFiles/basic.json +20 -0
  130. data/spec/fixtures/SolutionFiles/secret.json +6 -0
  131. data/spec/fixtures/configfile +9 -0
  132. data/spec/fixtures/dumped_config +42 -0
  133. data/spec/fixtures/mrmuranorc_deleted_bob +8 -0
  134. data/spec/fixtures/mrmuranorc_tool_bob +3 -0
  135. data/spec/fixtures/product_spec_files/example.exoline.spec.yaml +116 -0
  136. data/spec/fixtures/product_spec_files/example.murano.spec.yaml +14 -0
  137. data/spec/fixtures/product_spec_files/gwe.exoline.spec.yaml +21 -0
  138. data/spec/fixtures/product_spec_files/gwe.murano.spec.yaml +16 -0
  139. data/spec/fixtures/product_spec_files/lightbulb-no-state.yaml +11 -0
  140. data/spec/fixtures/product_spec_files/lightbulb.yaml +14 -0
  141. data/spec/fixtures/roles-three.yaml +11 -0
  142. data/spec/fixtures/syncable_content/assets/icon.png +0 -0
  143. data/spec/fixtures/syncable_content/assets/index.html +0 -0
  144. data/spec/fixtures/syncable_content/assets/js/script.js +0 -0
  145. data/spec/fixtures/syncable_content/modules/table_util.lua +58 -0
  146. data/spec/fixtures/syncable_content/routes/manyRoutes.lua +11 -0
  147. data/spec/fixtures/syncable_content/routes/singleRoute.lua +5 -0
  148. data/spec/fixtures/syncable_content/services/devdata.lua +18 -0
  149. data/spec/fixtures/syncable_content/services/timers.lua +4 -0
  150. data/spec/spec_helper.rb +119 -0
  151. metadata +498 -0
@@ -0,0 +1,150 @@
1
+ require 'uri'
2
+ require 'net/http'
3
+ require "http/form_data"
4
+ require 'digest/sha1'
5
+ require 'mime/types'
6
+ require 'pp'
7
+ require 'MrMurano/Solution'
8
+
9
+ module MrMurano
10
+ # …/file
11
+ class File < SolutionBase
12
+ def initialize
13
+ super
14
+ @uriparts << 'file'
15
+ @itemkey = :path
16
+ @project_section = :assets
17
+ end
18
+
19
+ ##
20
+ # Get a list of all of the static content
21
+ def list
22
+ get()
23
+ end
24
+
25
+ ##
26
+ # Get one item of the static content.
27
+ def fetch(path, &block)
28
+ get(path) do |request, http|
29
+ http.request(request) do |resp|
30
+ case resp
31
+ when Net::HTTPSuccess
32
+ if block_given? then
33
+ resp.read_body(&block)
34
+ else
35
+ resp.read_body do |chunk|
36
+ $stdout.write chunk
37
+ end
38
+ end
39
+ else
40
+ showHttpError(request, resp)
41
+ end
42
+ end
43
+ nil
44
+ end
45
+ end
46
+
47
+ ##
48
+ # Delete a file
49
+ def remove(path)
50
+ # TODO test
51
+ delete('/'+path)
52
+ end
53
+
54
+ def curldebug(request)
55
+ # The upload will get printed out inside of upload.
56
+ # Because we don't have the correct info here.
57
+ if request.method != 'PUT' then
58
+ super(request)
59
+ end
60
+ end
61
+
62
+ ##
63
+ # Upload a file
64
+ # @param modify Bool: True if item exists already and this is changing it
65
+ def upload(local, remote, modify)
66
+ local = Pathname.new(local) unless local.kind_of? Pathname
67
+
68
+ uri = endPoint('upload' + remote[:path])
69
+ # kludge past for a bit.
70
+ #`curl -s -H 'Authorization: token #{@token}' '#{uri.to_s}' -F file=@#{local.to_s}`
71
+
72
+ # http://stackoverflow.com/questions/184178/ruby-how-to-post-a-file-via-http-as-multipart-form-data
73
+ #
74
+ # Look at: https://github.com/httprb/http
75
+ # If it works well, consider porting over to it.
76
+ #
77
+ # Or just: https://github.com/httprb/form_data.rb ?
78
+ #
79
+ # Most of these pull into ram. So maybe just go with that. Would guess that
80
+ # truely large static content is rare, and we can optimize/fix that later.
81
+
82
+ file = HTTP::FormData::File.new(local.to_s, {:mime_type=>remote[:mime_type]})
83
+ form = HTTP::FormData.create(:file=>file)
84
+ req = Net::HTTP::Put.new(uri)
85
+ set_def_headers(req)
86
+ workit(req) do |request,http|
87
+ request.content_type = form.content_type
88
+ request.content_length = form.content_length
89
+ request.body = form.to_s
90
+
91
+ if $cfg['tool.curldebug'] then
92
+ a = []
93
+ a << %{curl -s -H 'Authorization: #{request['authorization']}'}
94
+ a << %{-H 'User-Agent: #{request['User-Agent']}'}
95
+ a << %{-X #{request.method}}
96
+ a << %{'#{request.uri.to_s}'}
97
+ a << %{-F file=@#{local.to_s}}
98
+ puts a.join(' ')
99
+ end
100
+
101
+ response = http.request(request)
102
+ case response
103
+ when Net::HTTPSuccess
104
+ else
105
+ showHttpError(request, response)
106
+ end
107
+ end
108
+ end
109
+
110
+ def tolocalname(item, key)
111
+ name = item[key]
112
+ name = $cfg['files.default_page'] if name == '/'
113
+ name
114
+ end
115
+
116
+ def toRemoteItem(from, path)
117
+ item = super(from, path)
118
+ name = item[:name]
119
+ name = '/' if name == $cfg['files.default_page']
120
+ name = "/#{name}" unless name.chars.first == '/'
121
+
122
+ mime = MIME::Types.type_for(path.to_s)[0] || MIME::Types["application/octet-stream"][0]
123
+
124
+ # It does not actually take the SHA1 of the file.
125
+ # It first converts the file to hex, then takes the SHA1 of that string
126
+ #sha1 = Digest::SHA1.file(path.to_s).hexdigest
127
+ sha1 = Digest::SHA1.new
128
+ path.open('rb:ASCII-8BIT') do |io|
129
+ while chunk = io.read(1048576) do
130
+ sha1 << Digest.hexencode(chunk)
131
+ end
132
+ end
133
+ debug "Checking #{name} (#{mime.simplified} #{sha1.hexdigest})"
134
+
135
+ {:path=>name, :mime_type=>mime.simplified, :checksum=>sha1.hexdigest}
136
+ end
137
+
138
+ def synckey(item)
139
+ item[:path]
140
+ end
141
+
142
+ def docmp(itemA, itemB)
143
+ return (itemA[:mime_type] != itemB[:mime_type] or
144
+ itemA[:checksum] != itemB[:checksum])
145
+ end
146
+
147
+ end
148
+ SyncRoot.add('files', File, 'S', %{Static Files}, true)
149
+ end
150
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,140 @@
1
+ require 'MrMurano/Solution'
2
+
3
+ module MrMurano
4
+ # …/serviceconfig
5
+ class ServiceConfig < SolutionBase
6
+ def initialize
7
+ super
8
+ @uriparts << 'serviceconfig'
9
+ @scid = nil
10
+ end
11
+
12
+ def list
13
+ get()[:items]
14
+ end
15
+ def fetch(id)
16
+ get('/' + id.to_s)
17
+ end
18
+
19
+ def scid_for_name(name)
20
+ name = name.to_s unless name.kind_of? String
21
+ scr = list().select{|i| i[:service] == name}.first
22
+ return nil if scr.nil?
23
+ scr[:id]
24
+ end
25
+
26
+ def scid
27
+ return @scid unless @scid.nil?
28
+ @scid = scid_for_name(@serviceName)
29
+ end
30
+
31
+ def info(id=scid)
32
+ get("/#{id}/info")
33
+ end
34
+
35
+ def logs(id=scid)
36
+ get("/#{id}/logs")
37
+ end
38
+
39
+ def call(opid, meth=:get, data=nil, id=scid, &block)
40
+ raise "Service '#{@serviceName}' not enabled for this Solution" if id.nil?
41
+ call = "/#{id.to_s}/call/#{opid.to_s}"
42
+ debug "Will call: #{call}"
43
+ case meth
44
+ when :get
45
+ get(call, data, &block)
46
+ when :post
47
+ data = {} if data.nil?
48
+ post(call, data, &block)
49
+ when :put
50
+ data = {} if data.nil?
51
+ put(call, data, &block)
52
+ when :delete
53
+ delete(call, &block)
54
+ else
55
+ raise "Unknown method: #{meth}"
56
+ end
57
+ end
58
+
59
+ end
60
+
61
+ ## This is only used for debugging and deciphering APIs.
62
+ #
63
+ # There was once a plan for using this to automagically map commands into
64
+ # services by reading their schema. That plan had too much magic and was too
65
+ # fragile for real use.
66
+ #
67
+ # A much better UI/UX happens with human intervention.
68
+ # :nocov:
69
+ class Services < SolutionBase
70
+ def initialize
71
+ super
72
+ @uriparts << 'service'
73
+ end
74
+
75
+ def sid_for_name(name)
76
+ name = name.to_s unless name.kind_of? String
77
+ scr = list().select{|i| i[:alias] == name}.first
78
+ scr[:id]
79
+ end
80
+
81
+ def sid
82
+ return @sid unless @sid.nil?
83
+ @sid = sid_for_name(@serviceName)
84
+ end
85
+
86
+ def list
87
+ ret = get()
88
+ ret[:items]
89
+ end
90
+
91
+ def schema(id=sid)
92
+ # TODO: cache schema in user dir?
93
+ get("/#{id}/schema")
94
+ end
95
+
96
+ ## Get list of call operations from a schema
97
+ def callable(id=sid)
98
+ scm = schema(id)
99
+ calls = []
100
+ scm[:paths].each do |path, methods|
101
+ methods.each do |method, params|
102
+ if params.kind_of?(Hash) and
103
+ not params['x-internal-use'.to_sym] and
104
+ params.has_key?(:operationId) then
105
+ calls << [method, params[:operationId]]
106
+ end
107
+ end
108
+ end
109
+ calls
110
+ end
111
+ end
112
+ # :nocov:
113
+
114
+ # Device config interface for the assign commands.
115
+ class SC_Device < ServiceConfig
116
+ def initialize
117
+ super
118
+ @serviceName = 'device'
119
+ end
120
+
121
+ def assignTriggers(products)
122
+ details = fetch(scid)
123
+ products = [products] unless products.kind_of? Array
124
+ details[:triggers] = {:pid=>products}
125
+ details[:parameters] = {:pid=>products}
126
+
127
+ put('/'+scid, details)
128
+ end
129
+
130
+ def showTriggers
131
+ details = fetch(scid)
132
+
133
+ return [] if details[:triggers].nil?
134
+ details[:triggers][:pid]
135
+ end
136
+
137
+ end
138
+ end
139
+
140
+ # vim: set ai et sw=2 ts=2 :
@@ -0,0 +1,326 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+ require 'net/http'
4
+ require 'json'
5
+ require 'yaml'
6
+ require 'date'
7
+ require 'digest/sha1'
8
+ require 'MrMurano/Solution'
9
+
10
+ module MrMurano
11
+ ##
12
+ # Things that servers do that is common.
13
+ class ServiceBase < SolutionBase
14
+
15
+ def mkalias(remote)
16
+ # :nocov:
17
+ raise "Needs to be implemented in child"
18
+ # :nocov:
19
+ end
20
+
21
+ def mkname(remote)
22
+ # :nocov:
23
+ raise "Needs to be implemented in child"
24
+ # :nocov:
25
+ end
26
+
27
+ def list
28
+ ret = get()
29
+ ret[:items]
30
+ end
31
+
32
+ def fetch(name)
33
+ raise "Missing name!" if name.nil?
34
+ raise "Empty name!" if name.empty?
35
+ ret = get('/'+CGI.escape(name))
36
+ error "Unexpected result type, assuming empty instead: #{ret}" unless ret.kind_of? Hash
37
+ ret = {} unless ret.kind_of? Hash
38
+ if block_given? then
39
+ yield (ret[:script] or '')
40
+ else
41
+ ret[:script] or ''
42
+ end
43
+ end
44
+
45
+ # ??? remove
46
+ def remove(name)
47
+ delete('/'+name)
48
+ end
49
+
50
+ # @param modify Bool: True if item exists already and this is changing it
51
+ def upload(local, remote, modify=false)
52
+ local = Pathname.new(local) unless local.kind_of? Pathname
53
+ raise "no file" unless local.exist?
54
+
55
+ # we assume these are small enough to slurp.
56
+ script = local.read
57
+
58
+ pst = remote.merge ({
59
+ :solution_id => $cfg['solution.id'],
60
+ :script => script,
61
+ :alias=>mkalias(remote),
62
+ :name=>mkname(remote),
63
+ })
64
+ debug "f: #{local} >> #{pst.reject{|k,_| k==:script}.to_json}"
65
+ # try put, if 404, then post.
66
+ put('/'+mkalias(remote), pst) do |request, http|
67
+ response = http.request(request)
68
+ case response
69
+ when Net::HTTPSuccess
70
+ #return JSON.parse(response.body)
71
+ when Net::HTTPNotFound
72
+ verbose "Doesn't exist, creating"
73
+ post('/', pst)
74
+ else
75
+ showHttpError(request, response)
76
+ end
77
+ end
78
+ cacheUpdateTimeFor(local)
79
+ end
80
+
81
+ def docmp(itemA, itemB)
82
+ if itemA[:updated_at].nil? and itemA[:local_path] then
83
+ ct = cachedUpdateTimeFor(itemA[:local_path])
84
+ itemA[:updated_at] = ct unless ct.nil?
85
+ itemA[:updated_at] = itemA[:local_path].mtime.getutc if ct.nil?
86
+ elsif itemA[:updated_at].kind_of? String then
87
+ itemA[:updated_at] = DateTime.parse(itemA[:updated_at]).to_time.getutc
88
+ end
89
+ if itemB[:updated_at].nil? and itemB[:local_path] then
90
+ ct = cachedUpdateTimeFor(itemB[:local_path])
91
+ itemB[:updated_at] = ct unless ct.nil?
92
+ itemB[:updated_at] = itemB[:local_path].mtime.getutc if ct.nil?
93
+ elsif itemB[:updated_at].kind_of? String then
94
+ itemB[:updated_at] = DateTime.parse(itemB[:updated_at]).to_time.getutc
95
+ end
96
+ return itemA[:updated_at].to_time.round != itemB[:updated_at].to_time.round
97
+ end
98
+
99
+ def cacheFileName
100
+ ['cache',
101
+ self.class.to_s.gsub(/\W+/,'_'),
102
+ @sid,
103
+ 'yaml'].join('.')
104
+ end
105
+
106
+ def cacheUpdateTimeFor(local_path, time=nil)
107
+ time = Time.now.getutc if time.nil?
108
+ entry = {
109
+ :sha1=>Digest::SHA1.file(local_path.to_s).hexdigest,
110
+ :updated_at=>time.to_datetime.iso8601(3)
111
+ }
112
+ cacheFile = $cfg.file_at(cacheFileName)
113
+ if cacheFile.file? then
114
+ cacheFile.open('r+') do |io|
115
+ cache = YAML.load(io)
116
+ cache = {} unless cache
117
+ io.rewind
118
+ cache[local_path.to_s] = entry
119
+ io << cache.to_yaml
120
+ end
121
+ else
122
+ cacheFile.open('w') do |io|
123
+ cache = {}
124
+ cache[local_path.to_s] = entry
125
+ io << cache.to_yaml
126
+ end
127
+ end
128
+ time
129
+ end
130
+
131
+ def cachedUpdateTimeFor(local_path)
132
+ cksm = Digest::SHA1.file(local_path.to_s).hexdigest
133
+ cacheFile = $cfg.file_at(cacheFileName)
134
+ return nil unless cacheFile.file?
135
+ ret = nil
136
+ cacheFile.open('r') do |io|
137
+ cache = YAML.load(io)
138
+ return nil unless cache
139
+ if cache.has_key?(local_path.to_s) then
140
+ entry = cache[local_path.to_s]
141
+ debug("For #{local_path}:")
142
+ debug(" cached: #{entry.to_s}")
143
+ debug(" cm: #{cksm}")
144
+ if entry.kind_of?(Hash) then
145
+ if entry[:sha1] == cksm and entry.has_key?(:updated_at) then
146
+ ret = DateTime.parse(entry[:updated_at])
147
+ end
148
+ end
149
+ end
150
+ end
151
+ ret
152
+ end
153
+ end
154
+
155
+ # …/library
156
+ class Library < ServiceBase
157
+ def initialize
158
+ super
159
+ @uriparts << 'library'
160
+ @itemkey = :alias
161
+ @project_section = :modules
162
+ end
163
+
164
+ def tolocalname(item, key)
165
+ name = item[:name]
166
+ "#{name}.lua"
167
+ end
168
+
169
+ def mkalias(remote)
170
+ if remote.has_key? :name then
171
+ [$cfg['solution.id'], remote[:name]].join('_')
172
+ else
173
+ raise "Missing parts! #{remote.to_json}"
174
+ end
175
+ end
176
+
177
+ def mkname(remote)
178
+ if remote.has_key? :name then
179
+ remote[:name]
180
+ else
181
+ raise "Missing parts! #{remote.to_json}"
182
+ end
183
+ end
184
+
185
+ def toRemoteItem(from, path)
186
+ name = path.basename.to_s.sub(/\..*/, '')
187
+ {:name => name}
188
+ end
189
+
190
+ def synckey(item)
191
+ item[:name]
192
+ end
193
+ end
194
+ SyncRoot.add('modules', Library, 'M', %{Modules}, true)
195
+
196
+ # …/eventhandler
197
+ class EventHandler < ServiceBase
198
+ def initialize
199
+ super
200
+ @uriparts << 'eventhandler'
201
+ @itemkey = :alias
202
+ @project_section = :services
203
+ @match_header = /--#EVENT (?<service>\S+) (?<event>\S+)/
204
+ end
205
+
206
+ def mkalias(remote)
207
+ if remote.has_key? :service and remote.has_key? :event then
208
+ [$cfg['solution.id'], remote[:service], remote[:event]].join('_')
209
+ else
210
+ raise "Missing parts! #{remote.to_json}"
211
+ end
212
+ end
213
+
214
+ def mkname(remote)
215
+ if remote.has_key? :service and remote.has_key? :event then
216
+ [remote[:service], remote[:event]].join('_')
217
+ else
218
+ raise "Missing parts! #{remote.to_json}"
219
+ end
220
+ end
221
+
222
+ def list
223
+ ret = get()
224
+ # eventhandler.skiplist is a list of whitespace seperated dot-paired values.
225
+ # fe: service.event service service service.event
226
+ skiplist = ($cfg['eventhandler.skiplist'] or '').split
227
+ ret[:items].reject { |i|
228
+ i.has_key?(:service) and i.has_key?(:event) and
229
+ ( skiplist.include? i[:service] or
230
+ skiplist.include? "#{i[:service]}.#{i[:event]}"
231
+ )
232
+ }
233
+ end
234
+
235
+ def fetch(name)
236
+ ret = get('/'+CGI.escape(name))
237
+ if ret.nil? then
238
+ error "Fetch for #{name} returned nil; skipping"
239
+ return ''
240
+ end
241
+ aheader = (ret[:script].lines.first or "").chomp
242
+ dheader = "--#EVENT #{ret[:service]} #{ret[:event]}"
243
+ if block_given? then
244
+ yield dheader + "\n" if aheader != dheader
245
+ yield ret[:script]
246
+ else
247
+ res = ''
248
+ res << dheader + "\n" if aheader != dheader
249
+ res << ret[:script]
250
+ res
251
+ end
252
+ end
253
+
254
+ def tolocalname(item, key)
255
+ "#{item[:name]}.lua"
256
+ end
257
+
258
+ def toRemoteItem(from, path)
259
+ # This allows multiple events to be in the same file. This is a lie.
260
+ # This only finds the last event in a file.
261
+ # :legacy support doesn't allow for that. but that's ok.
262
+ path = Pathname.new(path) unless path.kind_of? Pathname
263
+ cur = nil
264
+ lineno=0
265
+ path.readlines().each do |line|
266
+ md = @match_header.match(line)
267
+ if not md.nil? then
268
+ # header line.
269
+ cur = {:service=>md[:service],
270
+ :event=>md[:event],
271
+ :local_path=>path,
272
+ :line=>lineno,
273
+ :script=>line}
274
+ elsif not cur.nil? and not cur[:script].nil? then
275
+ cur[:script] << line
276
+ end
277
+ lineno += 1
278
+ end
279
+ cur[:line_end] = lineno unless cur.nil?
280
+
281
+ # If cur is nil here, then we need to do a :legacy check.
282
+ if cur.nil? and $project['services.legacy'].kind_of? Hash then
283
+ spath = path.relative_path_from(from)
284
+ debug "No headers: #{spath}"
285
+ service, event = $project['services.legacy'][spath.to_s]
286
+ debug "Legacy lookup #{spath} => [#{service}, #{event}]"
287
+ unless service.nil? or event.nil? then
288
+ warning "Event in #{spath} missing header, but has legacy support."
289
+ warning "Please add the header \"--#EVENT #{service} #{event}\""
290
+ cur = {:service=>service,
291
+ :event=>event,
292
+ :local_path=>path,
293
+ :line=>0,
294
+ :line_end => lineno,
295
+ :script=>path.read()} # FIXME: ick, fix this.
296
+ end
297
+ end
298
+ cur
299
+ end
300
+
301
+ def match(item, pattern)
302
+ # Pattern is: #{service}#{event}
303
+ pattern_pattern = /^#(?<service>[^#]*)#(?<event>.*)/i
304
+ md = pattern_pattern.match(pattern)
305
+ return false if md.nil?
306
+ debug "match pattern: '#{md[:service]}' '#{md[:event]}'"
307
+
308
+ unless md[:service].empty? then
309
+ return false unless item[:service].downcase == md[:service].downcase
310
+ end
311
+
312
+ unless md[:event].empty? then
313
+ return false unless item[:event].downcase == md[:event].downcase
314
+ end
315
+
316
+ true # Both match (or are empty.)
317
+ end
318
+
319
+ def synckey(item)
320
+ "#{item[:service]}_#{item[:event]}"
321
+ end
322
+ end
323
+ SyncRoot.add('eventhandlers', EventHandler, 'E', %{Event Handlers}, true)
324
+
325
+ end
326
+ # vim: set ai et sw=2 ts=2 :