strelka 0.0.1pre4 → 0.0.1.pre129

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 (73) hide show
  1. data/History.rdoc +1 -1
  2. data/IDEAS.rdoc +62 -0
  3. data/Manifest.txt +38 -7
  4. data/README.rdoc +124 -5
  5. data/Rakefile +22 -6
  6. data/bin/leash +102 -157
  7. data/contrib/hoetemplate/.autotest.erb +23 -0
  8. data/contrib/hoetemplate/History.rdoc.erb +4 -0
  9. data/contrib/hoetemplate/Manifest.txt.erb +8 -0
  10. data/contrib/hoetemplate/README.rdoc.erb +17 -0
  11. data/contrib/hoetemplate/Rakefile.erb +24 -0
  12. data/contrib/hoetemplate/data/file_name/apps/file_name_app +36 -0
  13. data/contrib/hoetemplate/data/file_name/templates/layout.tmpl.erb +13 -0
  14. data/contrib/hoetemplate/data/file_name/templates/top.tmpl.erb +8 -0
  15. data/contrib/hoetemplate/lib/file_name.rb.erb +18 -0
  16. data/contrib/hoetemplate/spec/file_name_spec.rb.erb +21 -0
  17. data/data/strelka/apps/hello-world +30 -0
  18. data/lib/strelka/app/defaultrouter.rb +49 -30
  19. data/lib/strelka/app/errors.rb +121 -0
  20. data/lib/strelka/app/exclusiverouter.rb +40 -0
  21. data/lib/strelka/app/filters.rb +18 -7
  22. data/lib/strelka/app/negotiation.rb +122 -0
  23. data/lib/strelka/app/parameters.rb +171 -14
  24. data/lib/strelka/app/paramvalidator.rb +751 -0
  25. data/lib/strelka/app/plugins.rb +66 -46
  26. data/lib/strelka/app/restresources.rb +499 -0
  27. data/lib/strelka/app/router.rb +73 -0
  28. data/lib/strelka/app/routing.rb +140 -18
  29. data/lib/strelka/app/templating.rb +12 -3
  30. data/lib/strelka/app.rb +174 -24
  31. data/lib/strelka/constants.rb +0 -20
  32. data/lib/strelka/exceptions.rb +29 -0
  33. data/lib/strelka/httprequest/acceptparams.rb +377 -0
  34. data/lib/strelka/httprequest/negotiation.rb +257 -0
  35. data/lib/strelka/httprequest.rb +155 -7
  36. data/lib/strelka/httpresponse/negotiation.rb +579 -0
  37. data/lib/strelka/httpresponse.rb +140 -0
  38. data/lib/strelka/logging.rb +4 -1
  39. data/lib/strelka/mixins.rb +53 -0
  40. data/lib/strelka.rb +22 -1
  41. data/spec/data/error.tmpl +1 -0
  42. data/spec/lib/constants.rb +0 -1
  43. data/spec/lib/helpers.rb +21 -0
  44. data/spec/strelka/app/defaultrouter_spec.rb +41 -35
  45. data/spec/strelka/app/errors_spec.rb +212 -0
  46. data/spec/strelka/app/exclusiverouter_spec.rb +220 -0
  47. data/spec/strelka/app/filters_spec.rb +196 -0
  48. data/spec/strelka/app/negotiation_spec.rb +73 -0
  49. data/spec/strelka/app/parameters_spec.rb +149 -0
  50. data/spec/strelka/app/paramvalidator_spec.rb +1059 -0
  51. data/spec/strelka/app/plugins_spec.rb +26 -19
  52. data/spec/strelka/app/restresources_spec.rb +393 -0
  53. data/spec/strelka/app/router_spec.rb +63 -0
  54. data/spec/strelka/app/routing_spec.rb +183 -9
  55. data/spec/strelka/app/templating_spec.rb +1 -2
  56. data/spec/strelka/app_spec.rb +265 -32
  57. data/spec/strelka/exceptions_spec.rb +53 -0
  58. data/spec/strelka/httprequest/acceptparams_spec.rb +282 -0
  59. data/spec/strelka/httprequest/negotiation_spec.rb +246 -0
  60. data/spec/strelka/httprequest_spec.rb +204 -14
  61. data/spec/strelka/httpresponse/negotiation_spec.rb +464 -0
  62. data/spec/strelka/httpresponse_spec.rb +114 -0
  63. data/spec/strelka/mixins_spec.rb +99 -0
  64. data.tar.gz.sig +1 -0
  65. metadata +175 -79
  66. metadata.gz.sig +2 -0
  67. data/IDEAS.textile +0 -174
  68. data/data/strelka/apps/strelka-admin +0 -65
  69. data/data/strelka/apps/strelka-setup +0 -26
  70. data/data/strelka/bootstrap-config.rb +0 -34
  71. data/data/strelka/templates/admin/console.tmpl +0 -21
  72. data/data/strelka/templates/layout.tmpl +0 -30
  73. data/lib/strelka/process.rb +0 -19
@@ -31,6 +31,26 @@ describe Strelka::App::Plugins do
31
31
  reset_logging()
32
32
  end
33
33
 
34
+ RSpec::Matchers.define( :order ) do |item|
35
+ match do |enumerable|
36
+ if defined?( @before )
37
+ enumerable.index( @before ) < enumerable.index( item )
38
+ elsif defined?( @after )
39
+ enumerable.index( @after ) > enumerable.index( item )
40
+ else
41
+ raise "No .before or .after to compare against!"
42
+ end
43
+ end
44
+
45
+ chain :before do |item|
46
+ @before = item
47
+ end
48
+
49
+ chain :after do |item|
50
+ @after = item
51
+ end
52
+ end
53
+
34
54
 
35
55
  describe "Plugin module" do
36
56
 
@@ -63,16 +83,9 @@ describe Strelka::App::Plugins do
63
83
  end
64
84
  end
65
85
 
66
- it "knows that it isn't after the other plugin" do
67
- @before_mod.should_not be_after( @other_mod )
68
- end
69
-
70
- it "knows that it is before the other plugin" do
71
- @before_mod.should be_before( @other_mod )
72
- end
73
-
74
- it "sorts before the other plugin" do
75
- [ @other_mod, @before_mod].sort.should == [ @before_mod, @other_mod ]
86
+ it "sorts before it in the plugin registry" do
87
+ Strelka::App.loaded_plugins.tsort.
88
+ should order( @other_mod.plugin_name ).before( @before_mod.plugin_name )
76
89
  end
77
90
 
78
91
  end
@@ -88,16 +101,10 @@ describe Strelka::App::Plugins do
88
101
  end
89
102
  end
90
103
 
91
- it "knows that it is after the other plugin" do
92
- @after_mod.should be_after( @other_mod )
93
- end
94
-
95
- it "knows that is isn't before the other plugin" do
96
- @after_mod.should_not be_before( @other_mod )
97
- end
98
104
 
99
- it "sorts after the other plugin" do
100
- [ @after_mod, @other_mod ].sort.should == [ @other_mod, @after_mod ]
105
+ it "sorts after it in the plugin registry" do
106
+ Strelka::App.loaded_plugins.tsort.
107
+ should order( @other_mod.plugin_name ).after( @after_mod.plugin_name )
101
108
  end
102
109
 
103
110
  end
@@ -0,0 +1,393 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ BEGIN {
4
+ require 'pathname'
5
+ basedir = Pathname.new( __FILE__ ).dirname.parent.parent.parent
6
+ $LOAD_PATH.unshift( basedir ) unless $LOAD_PATH.include?( basedir )
7
+ }
8
+
9
+ require 'rspec'
10
+
11
+ require 'spec/lib/helpers'
12
+
13
+ require 'strelka'
14
+ require 'strelka/app/plugins'
15
+ require 'strelka/app/restresources'
16
+
17
+ require 'strelka/behavior/plugin'
18
+ require 'mongrel2/config/dsl'
19
+
20
+
21
+ #####################################################################
22
+ ### C O N T E X T S
23
+ #####################################################################
24
+
25
+ describe Strelka::App::RestResources do
26
+ include Mongrel2::Config::DSL
27
+
28
+ before( :all ) do
29
+ setup_logging( :fatal )
30
+ setup_config_db()
31
+
32
+ @request_factory = Mongrel2::RequestFactory.new( route: '/api/v1' )
33
+ end
34
+
35
+ after( :all ) do
36
+ reset_logging()
37
+ end
38
+
39
+
40
+ it_should_behave_like( "A Strelka::App Plugin" )
41
+
42
+
43
+ describe "an including App" do
44
+
45
+ before( :each ) do
46
+ Strelka.log.debug "Creating a new Strelka::App"
47
+ @app = Class.new( Strelka::App ) do
48
+ plugin :restresources
49
+ def initialize( appid='rest-test', sspec=TEST_SEND_SPEC, rspec=TEST_RECV_SPEC )
50
+ super
51
+ end
52
+ end
53
+ end
54
+
55
+
56
+ it "knows what resources are mounted where" do
57
+ @app.resource_verbs.should be_a( Hash )
58
+ @app.resource_verbs.should be_empty()
59
+ end
60
+
61
+
62
+ describe "with a resource declared using default options" do
63
+
64
+ subject { @app }
65
+
66
+ before( :each ) do
67
+ @app.class_eval do
68
+ resource Mongrel2::Config::Server
69
+ end
70
+ end
71
+
72
+ it "knows about the mounted resource" do
73
+ @app.resource_verbs.should have( 1 ).member
74
+ @app.resource_verbs.should include( 'servers' )
75
+ @app.resource_verbs[ 'servers' ].
76
+ should include( :OPTIONS, :GET, :HEAD, :POST, :PUT, :DELETE )
77
+ end
78
+
79
+ # Reader regular routes
80
+ it { should have_route(:OPTIONS, 'servers') }
81
+ it { should have_route(:GET, 'servers') }
82
+ it { should have_route(:GET, 'servers/:id') }
83
+
84
+ # Writer regular routes
85
+ it { should have_route(:POST, 'servers') }
86
+ it { should have_route(:PUT, 'servers') }
87
+ it { should have_route(:PUT, 'servers/:id') }
88
+ it { should have_route(:DELETE, 'servers') }
89
+ it { should have_route(:DELETE, 'servers/:id') }
90
+
91
+ # Reader composite routes
92
+ it { should have_route(:GET, 'servers/by_uuid/:uuid') }
93
+ it { should have_route(:GET, 'servers/:id/hosts') }
94
+ it { should have_route(:GET, 'servers/:id/filters') }
95
+
96
+ end
97
+
98
+
99
+ describe "with a resource declared as read-only" do
100
+
101
+ subject { @app }
102
+
103
+ before( :each ) do
104
+ @app.class_eval do
105
+ resource Mongrel2::Config::Server, readonly: true
106
+ end
107
+ end
108
+
109
+ it "knows about the mounted resource" do
110
+ @app.resource_verbs.should have( 1 ).member
111
+ @app.resource_verbs.should include( 'servers' )
112
+ @app.resource_verbs[ 'servers' ].
113
+ should include( :OPTIONS, :GET, :HEAD )
114
+ @app.resource_verbs[ 'servers' ].
115
+ should_not include( :POST, :PUT, :DELETE )
116
+ end
117
+
118
+ # Reader regular routes
119
+ it { should have_route(:OPTIONS, 'servers') }
120
+ it { should have_route(:GET, 'servers') }
121
+ it { should have_route(:GET, 'servers/:id') }
122
+
123
+ # Writer regular routes
124
+ it { should_not have_route(:POST, 'servers') }
125
+ it { should_not have_route(:PUT, 'servers') }
126
+ it { should_not have_route(:PUT, 'servers/:id') }
127
+ it { should_not have_route(:DELETE, 'servers') }
128
+ it { should_not have_route(:DELETE, 'servers/:id') }
129
+
130
+ # Reader composite routes
131
+ it { should have_route(:GET, 'servers/by_uuid/:uuid') }
132
+ it { should have_route(:GET, 'servers/:id/hosts') }
133
+ it { should have_route(:GET, 'servers/:id/filters') }
134
+
135
+ end
136
+
137
+
138
+ describe "route behaviors" do
139
+
140
+ before( :each ) do
141
+ # Create two servers in the config db to test with
142
+ server 'test-server' do
143
+ host 'main'
144
+ host 'monitor'
145
+ host 'adminpanel'
146
+ host 'api'
147
+ end
148
+ server 'step-server'
149
+
150
+ @app.class_eval do
151
+ resource Mongrel2::Config::Server
152
+ end
153
+ end
154
+
155
+ after( :each ) do
156
+ # Clear the database after each test
157
+ Mongrel2::Config.subclasses.each {|klass| klass.truncate }
158
+ end
159
+
160
+ context "OPTIONS routes" do
161
+ it "responds to a top-level OPTIONS request with a resource description (JSON Schema?)"
162
+ it "responds to an OPTIONS request for a particular resource with details about it" do
163
+ req = @request_factory.options( '/api/v1/servers' )
164
+ res = @app.new.handle( req )
165
+
166
+ res.status.should == HTTP::OK
167
+ res.headers.allowed.split( /\s*,\s*/ ).should include(*%w[GET HEAD POST PUT DELETE])
168
+ end
169
+ end # OPTIONS routes
170
+
171
+
172
+ context "GET routes" do
173
+ it "has a GET route to fetch the resource collection" do
174
+ req = @request_factory.get( '/api/v1/servers', 'Accept' => 'application/json' )
175
+ res = @app.new.handle( req )
176
+
177
+ res.content_type.should == 'application/json'
178
+ body = Yajl.load( res.body )
179
+
180
+ body.should have( 2 ).members
181
+ body.map {|record| record['uuid'] }.should include( 'test-server', 'step-server' )
182
+ end
183
+
184
+ it "supports limiting the result set when fetching the resource collection" do
185
+ req = @request_factory.get( '/api/v1/servers?limit=1', 'Accept' => 'application/json' )
186
+ res = @app.new.handle( req )
187
+
188
+ res.status.should == HTTP::OK
189
+ res.content_type.should == 'application/json'
190
+ body = Yajl.load( res.body )
191
+
192
+ body.should have( 1 ).member
193
+ body[0]['uuid'].should == 'test-server'
194
+ end
195
+
196
+ it "supports paging the result set when fetching the resource collection" do
197
+ req = @request_factory.get( '/api/v1/servers?limit=1;offset=1', 'Accept' => 'application/json' )
198
+ res = @app.new.handle( req )
199
+
200
+ res.status.should == HTTP::OK
201
+ res.content_type.should == 'application/json'
202
+ body = Yajl.load( res.body )
203
+
204
+ body.should have( 1 ).member
205
+ body[0]['uuid'].should == 'step-server'
206
+ end
207
+
208
+ it "has a GET route to fetch a single resource by its ID" do
209
+ req = @request_factory.get( '/api/v1/servers/1', 'Accept' => 'application/json' )
210
+ res = @app.new.handle( req )
211
+
212
+ res.content_type.should == 'application/json'
213
+ body = Yajl.load( res.body )
214
+
215
+ body.should be_a( Hash )
216
+ body['uuid'].should == 'test-server'
217
+ end
218
+
219
+ it "returns a NOT FOUND response when fetching a non-existant resource" do
220
+ req = @request_factory.get( '/api/v1/servers/411', 'Accept' => 'application/json' )
221
+ res = @app.new.handle( req )
222
+
223
+ res.status.should == HTTP::NOT_FOUND
224
+ res.body.should =~ /no such server/i
225
+ end
226
+
227
+ it "returns a NOT FOUND response when fetching a resource with an invalid ID" do
228
+ req = @request_factory.get( '/api/v1/servers/ape-tastic' )
229
+ res = @app.new.handle( req )
230
+
231
+ res.status.should == HTTP::NOT_FOUND
232
+ res.body.should =~ /requested resource was not found/i
233
+ end
234
+
235
+ it "has a GET route for fetching the resource via one of its dataset methods" do
236
+ req = @request_factory.get( '/api/v1/servers/by_uuid/test-server', :accept => 'application/json' )
237
+ res = @app.new.handle( req )
238
+
239
+ res.status.should == HTTP::OK
240
+ body = Yajl.load( res.body )
241
+
242
+ body.should be_an( Array )
243
+ body.should have( 1 ).member
244
+ body.first.should be_a( Hash )
245
+ body.first['uuid'].should == 'test-server'
246
+ end
247
+
248
+ it "has a GET route for fetching the resource's associated objects" do
249
+ req = @request_factory.get( '/api/v1/servers/1/hosts' )
250
+ res = @app.new.handle( req )
251
+
252
+ res.status.should == HTTP::OK
253
+ body = Yajl.load( res.body )
254
+
255
+ body.should be_an( Array )
256
+ body.should have( 4 ).members
257
+ body.first.should be_a( Hash )
258
+ body.first['server_id'].should == 1
259
+ body.first['id'].should == 1
260
+ end
261
+
262
+ it "supports offset and limits for composite GET routes" do
263
+ req = @request_factory.get( '/api/v1/servers/1/hosts?offset=2;limit=2' )
264
+ res = @app.new.handle( req )
265
+
266
+ res.status.should == HTTP::OK
267
+ body = Yajl.load( res.body )
268
+
269
+ body.should be_an( Array )
270
+ body.should have( 2 ).members
271
+ body.first.should be_a( Hash )
272
+ body.first['server_id'].should == 1
273
+ body.first['id'].should == 3
274
+ end
275
+
276
+ end # GET routes
277
+
278
+
279
+ context "POST routes" do
280
+
281
+ before( :each ) do
282
+ @server_values = {
283
+ 'uuid' => "test-server",
284
+ 'access_log' => "/logs/admin-access.log",
285
+ 'error_log' => "/logs/admin-error.log",
286
+ 'chroot' => "/var/www",
287
+ 'pid_file' => "/var/run/test.pid",
288
+ 'default_host' => "localhost",
289
+ 'name' => "Testing Server",
290
+ 'bind_addr' => "127.0.0.1",
291
+ 'port' => '7337',
292
+ 'use_ssl' => '0',
293
+ }
294
+ @host_values = {
295
+ 'name' => 'step',
296
+ 'matching' => 'step.example.com',
297
+ }
298
+ end
299
+
300
+ it "has a POST route to create instances in the resource collection" do
301
+ req = @request_factory.post( '/api/v1/servers' )
302
+ req.content_type = 'application/json'
303
+ req.body = Yajl.dump( @server_values )
304
+
305
+ res = @app.new.handle( req )
306
+
307
+ res.status.should == HTTP::CREATED
308
+ res.headers.location.should == 'http://localhost:8080/api/v1/servers/3'
309
+
310
+ new_server = Mongrel2::Config::Server[ 3 ]
311
+
312
+ new_server.uuid.should == "test-server"
313
+ new_server.access_log.should == "/logs/admin-access.log"
314
+ new_server.error_log.should == "/logs/admin-error.log"
315
+ new_server.chroot.should == "/var/www"
316
+ new_server.pid_file.should == "/var/run/test.pid"
317
+ new_server.default_host.should == "localhost"
318
+ new_server.name.should == "Testing Server"
319
+ new_server.bind_addr.should == "127.0.0.1"
320
+ new_server.port.should == 7337
321
+ new_server.use_ssl.should == 0
322
+ end
323
+
324
+ end # POST routes
325
+
326
+
327
+ context "PUT routes" do
328
+
329
+ before( :each ) do
330
+ @posted_values = {
331
+ 'name' => 'Not A Testing Server',
332
+ 'bind_addr' => '0.0.0.0',
333
+ }
334
+ end
335
+
336
+ it "has a PUT route to update instances in the resource collection" do
337
+ req = @request_factory.put( '/api/v1/servers/1' )
338
+ req.content_type = 'application/json'
339
+ req.headers.accept = 'application/json'
340
+ req.body = Yajl.dump( @posted_values )
341
+
342
+ res = @app.new.handle( req )
343
+
344
+ res.status.should == HTTP::NO_CONTENT
345
+
346
+ Mongrel2::Config::Server[ 1 ].name.should == 'Not A Testing Server'
347
+ Mongrel2::Config::Server[ 1 ].bind_addr.should == '0.0.0.0'
348
+ end
349
+
350
+ it "has a PUT route to mass-update all resources in a collection" do
351
+ req = @request_factory.put( '/api/v1/servers' )
352
+ req.content_type = 'application/json'
353
+ req.body = Yajl.dump({ 'bind_addr' => '0.0.0.0' })
354
+
355
+ res = @app.new.handle( req )
356
+
357
+ res.status.should == HTTP::NO_CONTENT
358
+
359
+ Mongrel2::Config::Server.map( :bind_addr ).uniq.should == ['0.0.0.0']
360
+ end
361
+
362
+ end # PUT routes
363
+
364
+
365
+ context "DELETE routes" do
366
+
367
+ it "has a DELETE route to delete single instances in the resource collection" do
368
+ req = @request_factory.delete( '/api/v1/servers/1' )
369
+
370
+ res = @app.new.handle( req )
371
+
372
+ res.status.should == HTTP::NO_CONTENT
373
+
374
+ Mongrel2::Config::Server.count.should == 1
375
+ end
376
+
377
+ it "has a DELETE route to mass-delete all resources in a collection" do
378
+ req = @request_factory.delete( '/api/v1/servers' )
379
+
380
+ res = @app.new.handle( req )
381
+
382
+ res.status.should == HTTP::NO_CONTENT
383
+
384
+ Mongrel2::Config::Server.count.should == 0
385
+ end
386
+
387
+ end # DELETE routes
388
+
389
+ end # route behaviors
390
+
391
+ end
392
+
393
+ end
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ BEGIN {
4
+ require 'pathname'
5
+ basedir = Pathname.new( __FILE__ ).dirname.parent.parent.parent
6
+ $LOAD_PATH.unshift( basedir ) unless $LOAD_PATH.include?( basedir )
7
+ }
8
+
9
+ require 'rspec'
10
+
11
+ require 'spec/lib/helpers'
12
+
13
+ require 'strelka'
14
+ require 'strelka/app/router'
15
+
16
+
17
+ #####################################################################
18
+ ### C O N T E X T S
19
+ #####################################################################
20
+
21
+ describe Strelka::App::Router do
22
+
23
+ before( :all ) do
24
+ setup_logging( :fatal )
25
+ @request_factory = Mongrel2::RequestFactory.new( route: '/user' )
26
+ end
27
+
28
+ after( :all ) do
29
+ reset_logging()
30
+ end
31
+
32
+
33
+ it "looks for plugins under strelka/app" do
34
+ Strelka::App::Router.derivative_dirs.should include( 'strelka/app' )
35
+ end
36
+
37
+
38
+ it "is abstract" do
39
+ expect {
40
+ Strelka::App::Router.new
41
+ }.to raise_error()
42
+ end
43
+
44
+
45
+ describe "concrete subclasses" do
46
+
47
+ subject { Class.new(described_class).new }
48
+
49
+ it "raises NotImplementedErrors if they don't implement #add_route" do
50
+ expect {
51
+ subject.add_route(:GET, '', lambda {})
52
+ }.to raise_error(NotImplementedError)
53
+ end
54
+
55
+ it "raises NotImplementedErrors if they don't implement #route_request" do
56
+ expect {
57
+ subject.route_request(:request)
58
+ }.to raise_error(NotImplementedError)
59
+ end
60
+ end
61
+
62
+ end
63
+