MuranoCLI 2.0.0

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