lita-locker 0.3.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: e5c32b7506039ce73526e9483adfd4243e1aa36d
4
+ data.tar.gz: 745a609c8595fe3bd6cb8caab9de837b439f9049
5
+ SHA512:
6
+ metadata.gz: 3849f4ad7eec61bc12027688a35ff8002cac08dbcc8b0f0e478b7b3a31ea4f3e928ab33414c2e69fa28687dd005fb0cc0e9c3d7f986fb1b4a26de6fa871fcd84
7
+ data.tar.gz: 8408eff2c472f5d9f9141d91bed7bf6fa2a3c8e6d5a80e54d38a3eec20bba820d4d81ce78ea68fd2cbc886f7e3066b795e495fc733c9e5e14c934a39d1323ca0
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rubocop.yml ADDED
@@ -0,0 +1,19 @@
1
+ BlockNesting:
2
+ Max: 5
3
+
4
+ ClassLength:
5
+ Max: 500
6
+
7
+ CyclomaticComplexity:
8
+ Max: 10
9
+
10
+ Documentation:
11
+ Exclude:
12
+ - lib/lita/handlers/locker.rb
13
+
14
+ FileName:
15
+ Exclude:
16
+ - lib/lita-locker.rb
17
+
18
+ MethodLength:
19
+ Max: 200
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ script: bundle exec rake
5
+ before_install:
6
+ - gem update --system
7
+ services:
8
+ - redis-server
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2014 Eric Sigler
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # lita-locker
2
+
3
+ [![Build Status](https://img.shields.io/travis/esigler/lita-locker/master.svg)](https://travis-ci.org/esigler/lita-locker)
4
+ [![MIT License](https://img.shields.io/badge/license-MIT-brightgreen.svg)](https://tldrlegal.com/license/mit-license)
5
+ [![RubyGems :: RMuh Gem Version](http://img.shields.io/gem/v/lita-locker.svg)](https://rubygems.org/gems/lita-locker)
6
+ [![Coveralls Coverage](https://img.shields.io/coveralls/esigler/lita-locker/master.svg)](https://coveralls.io/r/esigler/lita-locker)
7
+ [![Code Climate](https://img.shields.io/codeclimate/github/esigler/lita-locker.svg)](https://codeclimate.com/github/esigler/lita-locker)
8
+ [![Gemnasium](https://img.shields.io/gemnasium/esigler/lita-locker.svg)](https://gemnasium.com/esigler/lita-locker)
9
+
10
+ Locking, unlocking shared resource handler for [lita.io](https://github.com/jimmycuadra/lita).
11
+
12
+ ## Installation
13
+
14
+ Add lita-locker to your Lita instance's Gemfile:
15
+
16
+ ``` ruby
17
+ gem "lita-locker"
18
+ ```
19
+
20
+ ## Configuration
21
+
22
+ None
23
+
24
+ ## Usage
25
+
26
+ lita-locker allows you to define resources (such as a server, or Git repo),
27
+ and labels (such as "production"). Labels can have multiple resources, and
28
+ resources can be referenced by multiple labels. A label can only be locked
29
+ if all of the resources it uses are available.
30
+
31
+ ### Examples
32
+ ```
33
+ lock web - Make something unavailable to others
34
+ lock web 30m - Make something unavailable to others for 30 minutes
35
+ unlock web - Make something available to others
36
+ locker reserve web - Make yourself the next owner of something after it is unlocked
37
+ locker status - Show what is and isn't available
38
+ ```
39
+
40
+ ### Locking, Unlocking, State
41
+ ```
42
+ lock <subject> - A basic reservation, with no time limit. <subject> can be a resource or a label.
43
+ unlock <subject> - Remove a reservation. This can only be done by whomever made the request.
44
+ unlock <subject> force - Force removal of a reservation. This can be done by anyone.
45
+ ```
46
+
47
+ ### Time-based locking - Not implemented yet!
48
+ ```
49
+ lock <subject> <time> - A time-limited reservation. <time> must be a number with a "s", "m", or "h" postfix.
50
+ ```
51
+
52
+ ### Reservations - Not implemented yet!
53
+ ```
54
+ reserve <subject> - Add yourself to a FIFO queue of pending reservations for <subject>
55
+ unreserve <subject> - Remove yourself from the queue for <subject>
56
+ ```
57
+
58
+ ### Labels
59
+ ```
60
+ locker label list - List all labels
61
+ locker label create <name> - Create a label with <name>. <name> must be A-Z, a-z, 0-9, no spaces or punctuation.
62
+ locker label delete <name> - Delete the label with <name>. Clears all locks associated.
63
+ locker label add <resource> to <name> - Adds <resource> to the list of things to lock/unlock for <name>
64
+ locker label remove <resource> from <name> - Removes <resource> from <name>
65
+ locker label show <name> - Show all resources for <name>
66
+ ```
67
+
68
+ ### Resources
69
+ ```
70
+ locker resource list - List all resources
71
+ locker resource create <name> - Create a resource with <name>. <name> must be A-Z, a-z, 0-9, no spaces or punctuation.
72
+ locker resource delete <name> - Delete the resource with <name>. Clears all locks associated.
73
+ locker resource show <name> - Show the state of <name>
74
+ ```
75
+
76
+ ### HTTP access
77
+ ```
78
+ curl http://lita.example.com/locker/label/<name> - Get current <name> status
79
+ curl http://lita.example.com/locker/resource/<name> - Get current <name> status
80
+ ```
81
+
82
+ ## License
83
+
84
+ [MIT](http://opensource.org/licenses/MIT)
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
3
+ require 'rubocop/rake_task'
4
+
5
+ RSpec::Core::RakeTask.new(:spec)
6
+ Rubocop::RakeTask.new(:rubocop)
7
+
8
+ task default: [:spec, :rubocop]
@@ -0,0 +1,9 @@
1
+ require 'lita'
2
+
3
+ Lita.load_locales Dir[File.expand_path(
4
+ File.join('..', '..', 'locales', '*.yml'), __FILE__
5
+ )]
6
+
7
+ require 'JSON'
8
+
9
+ require 'lita/handlers/locker'
@@ -0,0 +1,502 @@
1
+ module Lita
2
+ module Handlers
3
+ class Locker < Handler
4
+ http.get '/locker/label/:name', :http_label_show
5
+ http.get '/locker/resource/:name', :http_resource_show
6
+
7
+ # route(
8
+ # /^\(lock\)\s([a-zA-Z0-9_-]+)$/,
9
+ # :lock
10
+ # )
11
+
12
+ # route(
13
+ # /^\(unlock\)\s([a-zA-Z0-9_-]+)$/,
14
+ # :unlock
15
+ # )
16
+
17
+ route(
18
+ /^lock\s([a-zA-Z0-9_-]+)$/,
19
+ :lock,
20
+ command: true,
21
+ help: {
22
+ t('help.lock_key') => t('help.lock_value')
23
+ }
24
+ )
25
+
26
+ # route(
27
+ # /^lock\s([a-zA-Z0-9_-]+)\s(\d+)(s|m|h)$/,
28
+ # :lock,
29
+ # command: true,
30
+ # help: {
31
+ # t('help.lock_time_key') => t('help.lock_time_value')
32
+ # }
33
+ # )
34
+
35
+ route(
36
+ /^unlock\s([a-zA-Z0-9_-]+)$/,
37
+ :unlock,
38
+ command: true,
39
+ help: {
40
+ t('help.unlock_key') => t('help.unlock_value')
41
+ }
42
+ )
43
+
44
+ route(
45
+ /^unlock\s([a-zA-Z0-9_-]+)\sforce$/,
46
+ :unlock_force,
47
+ command: true,
48
+ help: {
49
+ t('help.unlock_force_key') => t('help.unlock_force_value')
50
+ }
51
+ )
52
+
53
+ route(
54
+ /^locker\sresource\slist$/,
55
+ :resource_list,
56
+ command: true,
57
+ help: {
58
+ t('help.resource_list_key') =>
59
+ t('help.resource_list_value')
60
+ }
61
+ )
62
+
63
+ route(
64
+ /^locker\sresource\screate\s([a-zA-Z0-9_-]+)$/,
65
+ :resource_create,
66
+ command: true,
67
+ help: {
68
+ t('help.resource_create_key') =>
69
+ t('help.resource_create_value')
70
+ }
71
+ )
72
+
73
+ route(
74
+ /^locker\sresource\sdelete\s([a-zA-Z0-9_-]+)$/,
75
+ :resource_delete,
76
+ command: true,
77
+ help: {
78
+ t('help.resource_delete_key') =>
79
+ t('help.resource_delete_value')
80
+ }
81
+ )
82
+
83
+ route(
84
+ /^locker\sresource\sshow\s([a-zA-Z0-9_-]+)$/,
85
+ :resource_show,
86
+ command: true,
87
+ help: {
88
+ t('help.resource_show_key') =>
89
+ t('help.resource_show_value')
90
+ }
91
+ )
92
+
93
+ route(
94
+ /^locker\slabel\slist$/,
95
+ :label_list,
96
+ command: true,
97
+ help: {
98
+ t('help.label_list_key') =>
99
+ t('help.label_list_value')
100
+ }
101
+ )
102
+
103
+ route(
104
+ /^locker\slabel\screate\s([a-zA-Z0-9_-]+)$/,
105
+ :label_create,
106
+ command: true,
107
+ help: {
108
+ t('help.label_create_key') =>
109
+ t('help.label_create_value')
110
+ }
111
+ )
112
+
113
+ route(
114
+ /^locker\slabel\sdelete\s([a-zA-Z0-9_-]+)$/,
115
+ :label_delete,
116
+ command: true,
117
+ help: {
118
+ t('help.label_delete_key') =>
119
+ t('help.label_delete_value')
120
+ }
121
+ )
122
+
123
+ route(
124
+ /^locker\slabel\sshow\s([a-zA-Z0-9_-]+)$/,
125
+ :label_show,
126
+ command: true,
127
+ help: {
128
+ t('help.label_show_key') =>
129
+ t('help.label_show_value')
130
+ }
131
+ )
132
+
133
+ route(
134
+ /^locker\slabel\sadd\s([a-zA-Z0-9_-]+)\sto\s([a-zA-Z0-9_-]+)$/,
135
+ :label_add,
136
+ command: true,
137
+ help: {
138
+ t('help.label_add_key') =>
139
+ t('help.label_add_value')
140
+ }
141
+ )
142
+
143
+ route(
144
+ /^locker\slabel\sremove\s([a-zA-Z0-9_-]+)\sfrom\s([a-zA-Z0-9_-]+)$/,
145
+ :label_remove,
146
+ command: true,
147
+ help: {
148
+ t('help.label_remove_key') =>
149
+ t('help.label_remove_value')
150
+ }
151
+ )
152
+
153
+ def http_label_show(request, response)
154
+ name = request.env['router.params'][:name]
155
+ response.headers['Content-Type'] = 'application/json'
156
+ result = label(name)
157
+ response.write(result.to_json)
158
+ end
159
+
160
+ def http_resource_show(request, response)
161
+ name = request.env['router.params'][:name]
162
+ response.headers['Content-Type'] = 'application/json'
163
+ result = resource(name)
164
+ response.write(result.to_json)
165
+ end
166
+
167
+ def lock(response)
168
+ name = response.matches[0][0]
169
+ timeamt = response.matches[0][1]
170
+ timeunit = response.matches[0][2]
171
+ case timeunit
172
+ when 's'
173
+ time_until = Time.now.utc + timeamt.to_i
174
+ when 'm'
175
+ time_until = Time.now.utc + (timeamt.to_i * 60)
176
+ when 'h'
177
+ time_until = Time.now.utc + (timeamt.to_i * 3600)
178
+ else
179
+ time_until = nil
180
+ end
181
+
182
+ if resource_exists?(name)
183
+ if lock_resource!(name, response.user, time_until)
184
+ response.reply(t('resource.lock', name: name))
185
+ else
186
+ response.reply(t('resource.is_locked', name: name))
187
+ end
188
+ elsif label_exists?(name)
189
+ m = label_membership(name)
190
+ if m.count > 0
191
+ if lock_label!(name, response.user, time_until)
192
+ response.reply(t('label.lock', name: name))
193
+ else
194
+ response.reply(t('label.unable_to_lock', name: name))
195
+ end
196
+ else
197
+ response.reply(t('label.no_resources', name: name))
198
+ end
199
+ else
200
+ response.reply(t('subject.does_not_exist', name: name))
201
+ end
202
+ end
203
+
204
+ def unlock(response)
205
+ name = response.matches[0][0]
206
+ if resource_exists?(name)
207
+ res = resource(name)
208
+ if res['state'] == 'unlocked'
209
+ response.reply(t('resource.is_unlocked', name: name))
210
+ else
211
+ # FIXME: NOT SECURE
212
+ if response.user.name == res['owner']
213
+ unlock_resource!(name)
214
+ response.reply(t('resource.unlock', name: name))
215
+ # FIXME: Handle the case where things can't be unlocked?
216
+ else
217
+ response.reply(t('resource.owned', name: name,
218
+ owner: res['owner']))
219
+ end
220
+ end
221
+ elsif label_exists?(name)
222
+ l = label(name)
223
+ if l['state'] == 'unlocked'
224
+ response.reply(t('label.is_unlocked', name: name))
225
+ else
226
+ # FIXME: NOT SECURE
227
+ if response.user.name == l['owner']
228
+ unlock_label!(name)
229
+ response.reply(t('label.unlock', name: name))
230
+ # FIXME: Handle the case where things can't be unlocked?
231
+ else
232
+ response.reply(t('label.owned', name: name,
233
+ owner: l['owner']))
234
+ end
235
+ end
236
+ else
237
+ response.reply(t('subject.does_not_exist', name: name))
238
+ end
239
+ end
240
+
241
+ def unlock_force(response)
242
+ name = response.matches[0][0]
243
+ if resource_exists?(name)
244
+ unlock_resource!(name)
245
+ response.reply(t('resource.unlock', name: name))
246
+ # FIXME: Handle the case where things can't be unlocked?
247
+ elsif label_exists?(name)
248
+ unlock_label!(name)
249
+ response.reply(t('label.unlock', name: name))
250
+ # FIXME: Handle the case where things can't be unlocked?
251
+ else
252
+ response.reply(t('subject.does_not_exist', name: name))
253
+ end
254
+ end
255
+
256
+ def label_list(response)
257
+ labels.each do |l|
258
+ response.reply(t('label.desc', name: l.sub('label_', '')))
259
+ end
260
+ end
261
+
262
+ def label_create(response)
263
+ name = response.matches[0][0]
264
+ if create_label(name)
265
+ response.reply(t('label.created', name: name))
266
+ else
267
+ response.reply(t('label.exists', name: name))
268
+ end
269
+ end
270
+
271
+ def label_delete(response)
272
+ name = response.matches[0][0]
273
+ if delete_label(name)
274
+ response.reply(t('label.deleted', name: name))
275
+ else
276
+ response.reply(t('label.does_not_exist', name: name))
277
+ end
278
+ end
279
+
280
+ def label_show(response)
281
+ name = response.matches[0][0]
282
+ if label_exists?(name)
283
+ members = label_membership(name)
284
+ if members.count > 0
285
+ response.reply(t('label.resources', name: name,
286
+ resources: members.join(', ')))
287
+ else
288
+ response.reply(t('label.has_no_resources', name: name))
289
+ end
290
+ else
291
+ response.reply(t('label.does_not_exist', name: name))
292
+ end
293
+ end
294
+
295
+ def label_add(response)
296
+ resource_name = response.matches[0][0]
297
+ label_name = response.matches[0][1]
298
+ if label_exists?(label_name)
299
+ if resource_exists?(resource_name)
300
+ add_resource_to_label(label_name, resource_name)
301
+ response.reply(t('label.resource_added', label: label_name,
302
+ resource: resource_name))
303
+ else
304
+ response.reply(t('resource.does_not_exist', name: resource_name))
305
+ end
306
+ else
307
+ response.reply(t('label.does_not_exist', name: label_name))
308
+ end
309
+ end
310
+
311
+ def label_remove(response)
312
+ resource_name = response.matches[0][0]
313
+ label_name = response.matches[0][1]
314
+ if label_exists?(label_name)
315
+ if resource_exists?(resource_name)
316
+ members = label_membership(label_name)
317
+ if members.include?(resource_name)
318
+ remove_resource_from_label(label_name, resource_name)
319
+ response.reply(t('label.resource_removed',
320
+ label: label_name, resource: resource_name))
321
+ else
322
+ response.reply(t('label.does_not_have_resource',
323
+ label: label_name, resource: resource_name))
324
+ end
325
+ else
326
+ response.reply(t('resource.does_not_exist', name: resource_name))
327
+ end
328
+ else
329
+ response.reply(t('label.does_not_exist', name: label_name))
330
+ end
331
+ end
332
+
333
+ def resource_list(response)
334
+ resources.each do |r|
335
+ r_name = r.sub('resource_', '')
336
+ res = resource(r_name)
337
+ response.reply(t('resource.desc', name: r_name, state: res['state']))
338
+ end
339
+ end
340
+
341
+ def resource_create(response)
342
+ name = response.matches[0][0]
343
+ if create_resource(name)
344
+ response.reply(t('resource.created', name: name))
345
+ else
346
+ response.reply(t('resource.exists', name: name))
347
+ end
348
+ end
349
+
350
+ def resource_delete(response)
351
+ name = response.matches[0][0]
352
+ if delete_resource(name)
353
+ response.reply(t('resource.deleted', name: name))
354
+ else
355
+ response.reply(t('resource.does_not_exist', name: name))
356
+ end
357
+ end
358
+
359
+ def resource_show(response)
360
+ name = response.matches[0][0]
361
+ if resource_exists?(name)
362
+ r = resource(name)
363
+ response.reply(t('resource.desc', name: name, state: r['state']))
364
+ else
365
+ response.reply(t('resource.does_not_exist', name: name))
366
+ end
367
+ end
368
+
369
+ private
370
+
371
+ def create_label(name)
372
+ label_key = "label_#{name}"
373
+ redis.hset(label_key, 'state', 'unlocked') unless
374
+ resource_exists?(name) || label_exists?(name)
375
+ end
376
+
377
+ def delete_label(name)
378
+ label_key = "label_#{name}"
379
+ redis.del(label_key) if label_exists?(name)
380
+ end
381
+
382
+ def label_exists?(name)
383
+ redis.exists("label_#{name}")
384
+ end
385
+
386
+ def label_membership(name)
387
+ redis.smembers("membership_#{name}")
388
+ end
389
+
390
+ def add_resource_to_label(label, resource)
391
+ if label_exists?(label) && resource_exists?(resource)
392
+ redis.sadd("membership_#{label}", resource)
393
+ end
394
+ end
395
+
396
+ def remove_resource_from_label(label, resource)
397
+ if label_exists?(label) && resource_exists?(resource)
398
+ redis.srem("membership_#{label}", resource)
399
+ end
400
+ end
401
+
402
+ def create_resource(name)
403
+ resource_key = "resource_#{name}"
404
+ redis.hset(resource_key, 'state', 'unlocked') unless
405
+ resource_exists?(name) || label_exists?(name)
406
+ end
407
+
408
+ def delete_resource(name)
409
+ resource_key = "resource_#{name}"
410
+ redis.del(resource_key) if resource_exists?(name)
411
+ end
412
+
413
+ def resource_exists?(name)
414
+ redis.exists("resource_#{name}")
415
+ end
416
+
417
+ def lock_resource!(name, owner, time_until)
418
+ if resource_exists?(name)
419
+ resource_key = "resource_#{name}"
420
+ value = redis.hget(resource_key, 'state')
421
+ if value == 'unlocked'
422
+ # FIXME: Race condition!
423
+ # FIXME: Store something better than name
424
+ redis.hset(resource_key, 'state', 'locked')
425
+ redis.hset(resource_key, 'owner', owner.name)
426
+ redis.hset(resource_key, 'until', time_until)
427
+ true
428
+ else
429
+ false
430
+ end
431
+ else
432
+ false
433
+ end
434
+ end
435
+
436
+ def lock_label!(name, owner, time_until)
437
+ if label_exists?(name)
438
+ key = "label_#{name}"
439
+ members = label_membership(name)
440
+ members.each do |m|
441
+ r = resource(m)
442
+ return false if r['state'] == 'locked'
443
+ end
444
+ # FIXME: No, really, race condition.
445
+ members.each do |m|
446
+ lock_resource!(m, owner, time_until)
447
+ end
448
+ redis.hset(key, 'state', 'locked')
449
+ redis.hset(key, 'owner', owner.name)
450
+ redis.hset(key, 'until', time_until)
451
+ true
452
+ else
453
+ false
454
+ end
455
+ end
456
+
457
+ def unlock_resource!(name)
458
+ if resource_exists?(name)
459
+ # FIXME: Tracking here?
460
+ key = "resource_#{name}"
461
+ redis.hset(key, 'state', 'unlocked')
462
+ redis.hset(key, 'owner', '')
463
+ else
464
+ false
465
+ end
466
+ end
467
+
468
+ def unlock_label!(name)
469
+ if label_exists?(name)
470
+ key = "label_#{name}"
471
+ members = label_membership(name)
472
+ members.each do |m|
473
+ unlock_resource!(m)
474
+ end
475
+ redis.hset(key, 'state', 'unlocked')
476
+ redis.hset(key, 'owner', '')
477
+ true
478
+ else
479
+ false
480
+ end
481
+ end
482
+
483
+ def resource(name)
484
+ redis.hgetall("resource_#{name}")
485
+ end
486
+
487
+ def resources
488
+ redis.keys('resource_*')
489
+ end
490
+
491
+ def label(name)
492
+ redis.hgetall("label_#{name}")
493
+ end
494
+
495
+ def labels
496
+ redis.keys('label_*')
497
+ end
498
+ end
499
+
500
+ Lita.register_handler(Locker)
501
+ end
502
+ end
@@ -0,0 +1,25 @@
1
+ Gem::Specification.new do |spec|
2
+ spec.name = 'lita-locker'
3
+ spec.version = '0.3.0'
4
+ spec.authors = ['Eric Sigler']
5
+ spec.email = ['me@esigler.com']
6
+ spec.description = %q("lock" and "unlock" arbitrary subjects)
7
+ spec.summary = %q("lock" and "unlock" arbitrary subjects)
8
+ spec.homepage = 'https://github.com/esigler/lita-locker'
9
+ spec.license = 'MIT'
10
+ spec.metadata = { 'lita_plugin_type' => 'handler' }
11
+
12
+ spec.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR)
13
+ spec.executables = spec.files.grep(/^bin\//) { |f| File.basename(f) }
14
+ spec.test_files = spec.files.grep(/^(test|spec|features)\//)
15
+ spec.require_paths = ['lib']
16
+
17
+ spec.add_runtime_dependency 'lita', '>= 3.2'
18
+
19
+ spec.add_development_dependency 'bundler', '~> 1.3'
20
+ spec.add_development_dependency 'rake'
21
+ spec.add_development_dependency 'rspec', '>= 3.0.0.beta2'
22
+ spec.add_development_dependency 'simplecov'
23
+ spec.add_development_dependency 'coveralls'
24
+ spec.add_development_dependency 'rubocop'
25
+ end
data/locales/en.yml ADDED
@@ -0,0 +1,61 @@
1
+ en:
2
+ lita:
3
+ handlers:
4
+ locker:
5
+ help:
6
+ lock_key: lock <subject>
7
+ lock_value: Make something unavailable to others
8
+ unlock_key: unlock <subject>
9
+ unlock_value: Make something available to others
10
+ unlock_force_key: unlock <subject> force
11
+ unlock_force_value: Force removal of a reservation.
12
+ resource_list_key: locker resource list
13
+ resource_list_value: List all resources
14
+ resource_create_key: locker resource create <name>
15
+ resource_create_value: Create a resource with <name>
16
+ resource_delete_key: locker resource delete <name>
17
+ resource_delete_value: Delete the resource with <name>
18
+ resource_show_key: locker resource show <name>
19
+ resource_show_value: Show the state of <name>
20
+ label_list_key: locker label list
21
+ label_list_value: List all labels
22
+ label_create_key: locker label create <name>
23
+ label_create_value: Create a label with <name>
24
+ label_delete_key: locker label delete <name>
25
+ label_delete_value: Delete the label with <name>
26
+ label_show_key: locker label show <name>
27
+ label_show_value: Show all resources for <name>
28
+ label_add_key: locker label add <resource> to <name>
29
+ label_add_value: Adds <resource> to the list of things to lock/unlock for <name>
30
+ label_remove_key: locker label remove <resource> from <name>
31
+ label_remove_value: Removes <resource> from <name>
32
+ resource:
33
+ created: "Resource %{name} created"
34
+ desc: "Resource: %{name}, state: %{state}"
35
+ exists: "%{name} already exists"
36
+ deleted: "Resource %{name} deleted"
37
+ does_not_exist: "Resource %{name} does not exist"
38
+ lock: "%{name} locked"
39
+ is_locked: "%{name} is locked"
40
+ unlock: "%{name} unlocked"
41
+ is_unlocked: "%{name} is unlocked"
42
+ owned: "%{name} is locked by %{owner}"
43
+ subject:
44
+ does_not_exist: "%{name} does not exist"
45
+ label:
46
+ unlock: "%{name} unlocked"
47
+ owned: "%{name} is locked by %{owner}"
48
+ is_unlocked: "%{name} is unlocked"
49
+ unable_to_lock: "%{name} unable to be locked"
50
+ lock: "%{name} locked"
51
+ desc: "Label: %{name}"
52
+ created: "Label %{name} created"
53
+ exists: "%{name} already exists"
54
+ deleted: "Label %{name} deleted"
55
+ does_not_exist: "Label %{name} does not exist"
56
+ has_no_resources: "Label %{name} has no resources"
57
+ resource_added: "Resource %{resource} has been added to %{label}"
58
+ resource_removed: "Resource %{resource} has been removed from %{label}"
59
+ resources: "Label %{name} has: %{resources}"
60
+ does_not_have_resource: "Label %{label} does not have Resource %{resource}"
61
+ no_resources: "%{name} has no resources, so it cannot be locked"
@@ -0,0 +1,352 @@
1
+ require 'spec_helper'
2
+
3
+ describe Lita::Handlers::Locker, lita_handler: true do
4
+ # it { routes('(lock) foobar').to(:lock) }
5
+ # it { routes('(unlock) foobar').to(:unlock) }
6
+
7
+ it { routes_command('lock foobar').to(:lock) }
8
+ # it { routes_command('lock foobar 30m').to(:lock) }
9
+
10
+ it { routes_command('unlock foobar').to(:unlock) }
11
+ it { routes_command('unlock foobar force').to(:unlock_force) }
12
+
13
+ it { routes_command('locker resource list').to(:resource_list) }
14
+ it { routes_command('locker resource create foobar').to(:resource_create) }
15
+ it { routes_command('locker resource delete foobar').to(:resource_delete) }
16
+ it { routes_command('locker resource show foobar').to(:resource_show) }
17
+
18
+ it { routes_command('locker label list').to(:label_list) }
19
+ it { routes_command('locker label create foobar').to(:label_create) }
20
+ it { routes_command('locker label delete foobar').to(:label_delete) }
21
+ it { routes_command('locker label show foobar').to(:label_show) }
22
+ it { routes_command('locker label add foo to bar').to(:label_add) }
23
+ it { routes_command('locker label remove foo from bar').to(:label_remove) }
24
+
25
+ it { routes_http(:get, '/locker/label/foobar').to(:http_label_show) }
26
+ it { routes_http(:get, '/locker/resource/foobar').to(:http_resource_show) }
27
+
28
+ describe '#lock' do
29
+ it 'locks a resource when it is available' do
30
+ send_command('locker resource create foobar')
31
+ send_command('lock foobar')
32
+ expect(replies.last).to eq('foobar locked')
33
+ end
34
+
35
+ it 'locks a label when it is available and has resources' do
36
+ send_command('locker resource create foobar')
37
+ send_command('locker label create bazbat')
38
+ send_command('locker label add foobar to bazbat')
39
+ send_command('lock bazbat')
40
+ expect(replies.last).to eq('bazbat locked')
41
+ send_command('locker resource show foobar')
42
+ expect(replies.last).to eq('Resource: foobar, state: locked')
43
+ end
44
+
45
+ it 'shows a warning when a label has no resources' do
46
+ send_command('locker label create foobar')
47
+ send_command('lock foobar')
48
+ expect(replies.last).to eq('foobar has no resources, ' \
49
+ 'so it cannot be locked')
50
+ end
51
+
52
+ it 'shows a warning when a resource is unavailable' do
53
+ send_command('locker resource create foobar')
54
+ send_command('lock foobar')
55
+ send_command('lock foobar')
56
+ expect(replies.last).to eq('foobar is locked')
57
+ end
58
+
59
+ it 'shows a warning when a label is unavailable' do
60
+ send_command('locker resource create foobar')
61
+ send_command('locker label create bazbat')
62
+ send_command('locker label add foobar to bazbat')
63
+ send_command('lock foobar')
64
+ send_command('lock bazbat')
65
+ expect(replies.last).to eq('bazbat unable to be locked')
66
+ end
67
+
68
+ it 'shows an error when a <subject> does not exist' do
69
+ send_command('lock foobar')
70
+ expect(replies.last).to eq('foobar does not exist')
71
+ end
72
+
73
+ # it 'locks a resource when it is available for a period of time' do
74
+ # send_command('locker resource create foobar')
75
+ # send_command('lock foobar 17m')
76
+ # expect(replies.last).to eq('foobar locked for 17 minutes')
77
+ # send_command('locker resource show foobar')
78
+ # expect(replies.last).to eq('Resource: foobar, state: locked')
79
+ # send_command('unlock foobar')
80
+ # send_command('lock foobar 12s')
81
+ # expect(replies.last).to eq('foobar locked for 17 seconds')
82
+ # send_command('unlock foobar')
83
+ # send_command('lock foobar 14h')
84
+ # expect(replies.last).to eq('foobar locked for 14 hours')
85
+ # end
86
+ end
87
+
88
+ describe '#unlock' do
89
+ it 'unlocks a resource when it is available' do
90
+ send_command('locker resource create foobar')
91
+ send_command('lock foobar')
92
+ send_command('unlock foobar')
93
+ expect(replies.last).to eq('foobar unlocked')
94
+ end
95
+
96
+ it 'does not unlock a resource when someone else locked it' do
97
+ alice = Lita::User.create(1, name: 'Alice')
98
+ bob = Lita::User.create(2, name: 'Bob')
99
+ send_command('locker resource create foobar')
100
+ send_command('lock foobar', as: alice)
101
+ send_command('unlock foobar', as: bob)
102
+ expect(replies.last).to eq('foobar is locked by Alice')
103
+ end
104
+
105
+ it 'unlocks a label when it is available' do
106
+ send_command('locker resource create foobar')
107
+ send_command('locker label create bazbat')
108
+ send_command('locker label add foobar to bazbat')
109
+ send_command('lock bazbat')
110
+ send_command('unlock bazbat')
111
+ expect(replies.last).to eq('bazbat unlocked')
112
+ end
113
+
114
+ it 'does not unlock a label when someone else locked it' do
115
+ alice = Lita::User.create(1, name: 'Alice')
116
+ bob = Lita::User.create(2, name: 'Bob')
117
+ send_command('locker resource create foobar')
118
+ send_command('locker label create bazbat')
119
+ send_command('locker label add foobar to bazbat')
120
+ send_command('lock bazbat', as: alice)
121
+ send_command('unlock bazbat', as: bob)
122
+ expect(replies.last).to eq('bazbat is locked by Alice')
123
+ end
124
+
125
+ it 'shows a warning when a resource is already unlocked' do
126
+ send_command('locker resource create foobar')
127
+ send_command('unlock foobar')
128
+ expect(replies.last).to eq('foobar is unlocked')
129
+ end
130
+
131
+ it 'shows a warning when a label is already unlocked' do
132
+ send_command('locker resource create foobar')
133
+ send_command('locker label create bazbat')
134
+ send_command('locker label add foobar to bazbat')
135
+ send_command('unlock bazbat')
136
+ send_command('unlock bazbat')
137
+ expect(replies.last).to eq('bazbat is unlocked')
138
+ end
139
+
140
+ it 'shows an error when a <subject> does not exist' do
141
+ send_command('unlock foobar')
142
+ expect(replies.last).to eq('foobar does not exist')
143
+ end
144
+ end
145
+
146
+ describe '#unlock_force' do
147
+ it 'unlocks a resource from someone else when it is available' do
148
+ alice = Lita::User.create(1, name: 'Alice')
149
+ bob = Lita::User.create(2, name: 'Bob')
150
+ send_command('locker resource create foobar')
151
+ send_command('lock foobar', as: alice)
152
+ send_command('unlock foobar force', as: bob)
153
+ expect(replies.last).to eq('foobar unlocked')
154
+ end
155
+
156
+ it 'unlocks a label from someone else when it is available' do
157
+ alice = Lita::User.create(1, name: 'Alice')
158
+ bob = Lita::User.create(2, name: 'Bob')
159
+ send_command('locker resource create foobar')
160
+ send_command('locker label create bazbat')
161
+ send_command('locker label add foobar to bazbat')
162
+ send_command('lock bazbat', as: alice)
163
+ send_command('unlock bazbat force', as: bob)
164
+ expect(replies.last).to eq('bazbat unlocked')
165
+ end
166
+
167
+ it 'shows an error when a <subject> does not exist' do
168
+ send_command('unlock foobar force')
169
+ expect(replies.last).to eq('foobar does not exist')
170
+ end
171
+ end
172
+
173
+ describe '#label_list' do
174
+ it 'shows a list of labels if there are any' do
175
+ send_command('locker label create foobar')
176
+ send_command('locker label create bazbat')
177
+ send_command('locker label list')
178
+ expect(replies.last).to eq('Label: bazbat')
179
+ end
180
+ end
181
+
182
+ describe '#label_create' do
183
+ it 'creates a label with <name>' do
184
+ send_command('locker label create foobar')
185
+ expect(replies.last).to eq('Label foobar created')
186
+ end
187
+
188
+ it 'shows a warning when the <name> already exists as a label' do
189
+ send_command('locker label create foobar')
190
+ send_command('locker label create foobar')
191
+ expect(replies.last).to eq('foobar already exists')
192
+ end
193
+
194
+ it 'shows a warning when the <name> already exists as a resource' do
195
+ send_command('locker resource create foobar')
196
+ send_command('locker label create foobar')
197
+ expect(replies.last).to eq('foobar already exists')
198
+ end
199
+ end
200
+
201
+ describe '#label_delete' do
202
+ it 'deletes a label with <name>' do
203
+ send_command('locker label create foobar')
204
+ send_command('locker label delete foobar')
205
+ expect(replies.last).to eq('Label foobar deleted')
206
+ end
207
+
208
+ it 'shows a warning when <name> does not exist' do
209
+ send_command('locker label delete foobar')
210
+ expect(replies.last).to eq('Label foobar does not exist')
211
+ end
212
+ end
213
+
214
+ describe '#label_show' do
215
+ it 'shows a list of resources for a label if there are any' do
216
+ send_command('locker resource create whatever')
217
+ send_command('locker label create foobar')
218
+ send_command('locker label add whatever to foobar')
219
+ send_command('locker label show foobar')
220
+ expect(replies.last).to eq('Label foobar has: whatever')
221
+ end
222
+
223
+ it 'shows a warning if there are no resources for the label' do
224
+ send_command('locker label create foobar')
225
+ send_command('locker label show foobar')
226
+ expect(replies.last).to eq('Label foobar has no resources')
227
+ end
228
+
229
+ it 'shows an error if the label does not exist' do
230
+ send_command('locker label show foobar')
231
+ expect(replies.last).to eq('Label foobar does not exist')
232
+ end
233
+ end
234
+
235
+ describe '#label_add' do
236
+ it 'adds a resource to a label if both exist' do
237
+ send_command('locker resource create foo')
238
+ send_command('locker label create bar')
239
+ send_command('locker label add foo to bar')
240
+ expect(replies.last).to eq('Resource foo has been added to bar')
241
+ send_command('locker label show bar')
242
+ expect(replies.last).to eq('Label bar has: foo')
243
+ end
244
+
245
+ it 'adds multiple resources to a label if all exist' do
246
+ send_command('locker resource create foo')
247
+ send_command('locker resource create baz')
248
+ send_command('locker label create bar')
249
+ send_command('locker label add foo to bar')
250
+ send_command('locker label add baz to bar')
251
+ send_command('locker label show bar')
252
+ expect(replies.last).to eq('Label bar has: baz, foo')
253
+ end
254
+
255
+ it 'shows an error if the label does not exist' do
256
+ send_command('locker label add foo to bar')
257
+ expect(replies.last).to eq('Label bar does not exist')
258
+ end
259
+
260
+ it 'shows an error if the resource does not exist' do
261
+ send_command('locker label create bar')
262
+ send_command('locker label add foo to bar')
263
+ expect(replies.last).to eq('Resource foo does not exist')
264
+ end
265
+ end
266
+
267
+ describe '#label_remove' do
268
+ it 'removes a resource from a label if both exist and are related' do
269
+ send_command('locker resource create foo')
270
+ send_command('locker label create bar')
271
+ send_command('locker label add foo to bar')
272
+ send_command('locker label remove foo from bar')
273
+ send_command('locker label show bar')
274
+ expect(replies.last).to eq('Label bar has no resources')
275
+ end
276
+
277
+ it 'shows an error if they both exist but are not related' do
278
+ send_command('locker resource create foo')
279
+ send_command('locker label create bar')
280
+ send_command('locker label remove foo from bar')
281
+ expect(replies.last).to eq('Label bar does not have Resource foo')
282
+ end
283
+
284
+ it 'shows an error if the label does not exist' do
285
+ send_command('locker label add foo to bar')
286
+ expect(replies.last).to eq('Label bar does not exist')
287
+ end
288
+
289
+ it 'shows an error if the resource does not exist' do
290
+ send_command('locker label create bar')
291
+ send_command('locker label add foo to bar')
292
+ expect(replies.last).to eq('Resource foo does not exist')
293
+ end
294
+ end
295
+
296
+ describe '#resource_list' do
297
+ it 'shows a list of resources if there are any' do
298
+ send_command('locker resource create foobar')
299
+ send_command('locker resource create bazbat')
300
+ send_command('locker resource list')
301
+ expect(replies.last).to eq('Resource: foobar, state: unlocked')
302
+ end
303
+ end
304
+
305
+ describe '#resource_create' do
306
+ it 'creates a resource with <name>' do
307
+ send_command('locker resource create foobar')
308
+ expect(replies.last).to eq('Resource foobar created')
309
+ end
310
+
311
+ it 'shows a warning when the <name> already exists as a resource' do
312
+ send_command('locker resource create foobar')
313
+ send_command('locker resource create foobar')
314
+ expect(replies.last).to eq('foobar already exists')
315
+ end
316
+
317
+ it 'shows a warning when the <name> already exists as a label' do
318
+ send_command('locker label create foobar')
319
+ send_command('locker resource create foobar')
320
+ expect(replies.last).to eq('foobar already exists')
321
+ end
322
+ end
323
+
324
+ describe '#resource_delete' do
325
+ it 'deletes a resource with <name>' do
326
+ send_command('locker resource create foobar')
327
+ send_command('locker resource delete foobar')
328
+ expect(replies.last).to eq('Resource foobar deleted')
329
+ end
330
+
331
+ it 'shows a warning when <name> does not exist' do
332
+ send_command('locker resource delete foobar')
333
+ expect(replies.last).to eq('Resource foobar does not exist')
334
+ end
335
+ end
336
+
337
+ describe '#resource_show' do
338
+ it 'shows the state of a <name> if it exists' do
339
+ send_command('locker resource create foobar')
340
+ send_command('locker resource show foobar')
341
+ expect(replies.last).to eq('Resource: foobar, state: unlocked')
342
+ send_command('lock foobar')
343
+ send_command('locker resource show foobar')
344
+ expect(replies.last).to eq('Resource: foobar, state: locked')
345
+ end
346
+
347
+ it 'shows a warning when <name> does not exist' do
348
+ send_command('locker resource show foobar')
349
+ expect(replies.last).to eq('Resource foobar does not exist')
350
+ end
351
+ end
352
+ end
@@ -0,0 +1,10 @@
1
+ require 'simplecov'
2
+ require 'coveralls'
3
+ SimpleCov.formatter = SimpleCov::Formatter::MultiFormatter[
4
+ SimpleCov::Formatter::HTMLFormatter,
5
+ Coveralls::SimpleCov::Formatter
6
+ ]
7
+ SimpleCov.start { add_filter '/spec/' }
8
+
9
+ require 'lita-locker'
10
+ require 'lita/rspec'
metadata ADDED
@@ -0,0 +1,158 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: lita-locker
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.3.0
5
+ platform: ruby
6
+ authors:
7
+ - Eric Sigler
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2014-06-16 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: lita
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '3.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '3.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.3'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.0.0.beta2
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: 3.0.0.beta2
69
+ - !ruby/object:Gem::Dependency
70
+ name: simplecov
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: coveralls
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: rubocop
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: "\"lock\" and \"unlock\" arbitrary subjects"
112
+ email:
113
+ - me@esigler.com
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".gitignore"
119
+ - ".rubocop.yml"
120
+ - ".travis.yml"
121
+ - Gemfile
122
+ - LICENSE
123
+ - README.md
124
+ - Rakefile
125
+ - lib/lita-locker.rb
126
+ - lib/lita/handlers/locker.rb
127
+ - lita-locker.gemspec
128
+ - locales/en.yml
129
+ - spec/lita/handlers/locker_spec.rb
130
+ - spec/spec_helper.rb
131
+ homepage: https://github.com/esigler/lita-locker
132
+ licenses:
133
+ - MIT
134
+ metadata:
135
+ lita_plugin_type: handler
136
+ post_install_message:
137
+ rdoc_options: []
138
+ require_paths:
139
+ - lib
140
+ required_ruby_version: !ruby/object:Gem::Requirement
141
+ requirements:
142
+ - - ">="
143
+ - !ruby/object:Gem::Version
144
+ version: '0'
145
+ required_rubygems_version: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ requirements: []
151
+ rubyforge_project:
152
+ rubygems_version: 2.2.1
153
+ signing_key:
154
+ specification_version: 4
155
+ summary: "\"lock\" and \"unlock\" arbitrary subjects"
156
+ test_files:
157
+ - spec/lita/handlers/locker_spec.rb
158
+ - spec/spec_helper.rb