lotus-controller 0.0.0 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: a3c6e1b5ad5fb6ed2599cf088d46e20ba247e4a8
4
- data.tar.gz: 458c9f178f985f4ca52c1a6d3781b71bd52a3662
3
+ metadata.gz: fac98fda3029902773f0dc45d886942f4e3fae87
4
+ data.tar.gz: b2a40a8361d84bb0625aaf8d60925952999704ce
5
5
  SHA512:
6
- metadata.gz: 5e2e9e5166af93755eb7b9d9665bcd4c0bad5fa42d7a4f8acbb409d5b5e4c75d6c2a0bc4a1d98d5c277e10ba8b8b5b0238c6a4fa2fac8e09d5bfb3105363dbc0
7
- data.tar.gz: ce6975e1c68a7ffbf482d5be0c4a55a668d47c7d170cd842aff6cd1f04a211dbabe5a0b654781b3536061a2e83fd115610c554a86f9659b6b8347e94dfa81590
6
+ metadata.gz: 8f3c1d85c792b625120de32d1210dcd080501a6b041a5ccc3ee9d6788cb44bbe0354162c097f9488acdc25ddd841fa1378e9d24547832ba4b61b83edb4e44251
7
+ data.tar.gz: 5080fe24a7c4260054730dddf65ebcf10803c45ddd4f95981984971cc81a9e635ef723fcffbc19150226125fc34ce3d9c9003b57ce59fbdd3f9f330b4b8c8104
data/.gitignore CHANGED
@@ -1,17 +1,10 @@
1
- *.gem
2
- *.rbc
3
- .bundle
4
- .config
5
- .yardoc
1
+ .devnotes
2
+ .greenbar
6
3
  Gemfile.lock
7
- InstalledFiles
8
- _yardoc
4
+ tags
5
+ .bundle
9
6
  coverage
10
- doc/
11
- lib/bundler/man
12
- pkg
13
- rdoc
14
- spec/reports
15
- test/tmp
16
- test/version_tmp
17
7
  tmp
8
+ doc/
9
+ .yardoc
10
+ lotus-controller-0.1.0.gem
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ language: ruby
2
+ script: 'bundle exec rake test:coverage'
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.0
data/.yardopts ADDED
@@ -0,0 +1,4 @@
1
+ --protected
2
+ -
3
+ LICENSE.txt
4
+ lib/**/*.rb
data/Gemfile CHANGED
@@ -1,4 +1,15 @@
1
- source 'https://rubygems.org'
1
+ source 'http://rubygems.org'
2
2
 
3
- # Specify your gem's dependencies in lotus-controller.gemspec
4
3
  gemspec
4
+
5
+ if !ENV['TRAVIS']
6
+ gem 'debugger', require: false
7
+ gem 'yard', require: false
8
+ gem 'lotus-utils', require: false, path: '../lotus-utils'
9
+ gem 'lotus-router', require: false, path: '../lotus-router'
10
+ else
11
+ gem 'lotus-router', require: false
12
+ end
13
+
14
+ gem 'simplecov', require: false
15
+ gem 'coveralls', require: false
data/README.md CHANGED
@@ -1,29 +1,650 @@
1
1
  # Lotus::Controller
2
2
 
3
- TODO: Write a gem description
3
+ A Rack compatible Controller layer for [Lotus](http://lotusrb.org).
4
+
5
+ ## Status
6
+
7
+ [![Gem Version](https://badge.fury.io/rb/lotus-controller.png)](http://badge.fury.io/rb/lotus-controller)
8
+ [![Build Status](https://secure.travis-ci.org/lotus/controller.png?branch=master)](http://travis-ci.org/lotus/controller?branch=master)
9
+ [![Coverage](https://coveralls.io/repos/lotus/controller/badge.png?branch=master)](https://coveralls.io/r/lotus/controller)
10
+ [![Code Climate](https://codeclimate.com/github/lotus/controller.png)](https://codeclimate.com/github/lotus/controller)
11
+ [![Dependencies](https://gemnasium.com/lotus/controller.png)](https://gemnasium.com/lotus/controller)
12
+
13
+ ## Contact
14
+
15
+ * Home page: http://lotusrb.org
16
+ * Mailing List: http://lotusrb.org/mailing-list
17
+ * API Doc: http://rdoc.info/gems/lotus-controller
18
+ * Bugs/Issues: https://github.com/lotus/controller/issues
19
+ * Support: http://stackoverflow.com/questions/tagged/lotusrb
20
+
21
+ ## Rubies
22
+
23
+ __Lotus::Controller__ supports Ruby (MRI) 2+
4
24
 
5
25
  ## Installation
6
26
 
7
27
  Add this line to your application's Gemfile:
8
28
 
9
- gem 'lotus-controller'
29
+ ```ruby
30
+ gem 'lotus-controller'
31
+ ```
10
32
 
11
33
  And then execute:
12
34
 
13
- $ bundle
35
+ ```shell
36
+ $ bundle
37
+ ```
14
38
 
15
39
  Or install it yourself as:
16
40
 
17
- $ gem install lotus-controller
41
+ ```shell
42
+ $ gem install lotus-controller
43
+ ```
18
44
 
19
45
  ## Usage
20
46
 
21
- TODO: Write usage instructions here
47
+ Lotus::Controller is a thin layer (**275 LOCs**) for MVC web frameworks.
48
+ It works beautifully with [Lotus::Router](https://github.com/lotus/router), but it can be employed everywhere.
49
+ It's designed to be fast and testable.
50
+
51
+ ### Actions
52
+
53
+ The core of this frameworks are the actions.
54
+ They are the endpoint that responds to incoming HTTP requests.
55
+
56
+ ```ruby
57
+ class Show
58
+ include Lotus::Action
59
+
60
+ def call(params)
61
+ @article = Article.find params[:id]
62
+ end
63
+ end
64
+ ```
65
+
66
+ The usage of `Lotus::Action` follows the Lotus philosophy: include a module and implement a minimal interface.
67
+ In this case, it's only one method: `#call(params)`.
68
+
69
+ Lotus is designed to not interfere with inheritance.
70
+ This is important, because you can implement your own initialization strategy.
71
+
72
+ __An action is an object__ after all, it's important that __you have the full control on it__.
73
+ In other words, you have the freedom of instantiate, inject dependencies and test it, both with unit and integration.
74
+
75
+ In the example below, we're stating that the default repository is `Article`, but during an unit test we can inject a stubbed version, and invoke `#call` with the params that we want to simulate.
76
+ __We're avoiding HTTP calls__, we're eventually avoiding to hit the database (it depends on the stubbed repository), __we're just dealing with message passing__.
77
+ Imagine how **fast** can be a unit test like this.
78
+
79
+ ```ruby
80
+ class Show
81
+ include Lotus::Action
82
+
83
+ def initialize(repository = Article)
84
+ @repository = repository
85
+ end
86
+
87
+ def call(params)
88
+ @article = @repository.find params[:id]
89
+ end
90
+ end
91
+
92
+ action = Show.new(MemoryArticleRepository)
93
+ action.call({ id: 23 })
94
+ ```
95
+
96
+ ### Params
97
+
98
+ The request params are passed as an argument to the `#call` method.
99
+ If routed with *Lotus::Router*, it extracts the relevant bits from the Rack `env` (eg the requested `:id`).
100
+ Otherwise everything it's passed as it is: the full Rack `env` in production, and the given `Hash` for unit tests.
101
+
102
+ With Lotus::Router:
103
+
104
+ ```ruby
105
+ class Show
106
+ include Lotus::Action
107
+
108
+ def call(params)
109
+ # ...
110
+ puts params # => { id: 23 } extracted from Rack env
111
+ end
112
+ end
113
+ ```
114
+
115
+ Standalone:
116
+
117
+ ```ruby
118
+ class Show
119
+ include Lotus::Action
120
+
121
+ def call(params)
122
+ # ...
123
+ puts params # => { :"rack.version"=>[1, 2], :"rack.input"=>#<StringIO:0x007fa563463948>, ... }
124
+ end
125
+ end
126
+ ```
127
+
128
+ Unit Testing:
129
+
130
+ ```ruby
131
+ class Show
132
+ include Lotus::Action
133
+
134
+ def call(params)
135
+ # ...
136
+ puts params # => { id: 23, key: 'value' } passed as it is from testing
137
+ end
138
+ end
139
+
140
+ action = Show.new
141
+ response = action.call({ id: 23, key: 'value' })
142
+ ```
143
+
144
+ ### Response
145
+
146
+ The output of `#call` is a serialized Rack::Response (see [#finish](http://rack.rubyforge.org/doc/classes/Rack/Response.html#M000182)):
147
+
148
+ ```ruby
149
+ class Show
150
+ include Lotus::Action
151
+
152
+ def call(params)
153
+ # ...
154
+ end
155
+ end
156
+
157
+ action = Show.new
158
+ action.call({}) # => [200, {}, [""]]
159
+ ```
160
+
161
+ It has private accessors to explicitly set status, headers and body:
162
+
163
+ ```ruby
164
+ class Show
165
+ include Lotus::Action
166
+
167
+ def call(params)
168
+ self.status = 201
169
+ self.body = 'Hi!'
170
+ self.headers.merge!({ 'X-Custom' => 'OK' })
171
+ end
172
+ end
173
+
174
+ action = Show.new
175
+ action.call({}) # => [201, { "X-Custom" => "OK" }, ["Hi!"]]
176
+ ```
177
+ ### Exposures
178
+
179
+ We know that actions are objects and Lotus::Action respects one of the pillars of OOP: __encapsulation__.
180
+ Other frameworks extract instance variables (`@ivar`) and make them available to the view context.
181
+ The solution of Lotus::Action is a simple and powerful DSL: `expose`.
182
+ It's a thin layer on top of `attr_reader`. When used, it creates a getter for the given attribute, and adds it to the _exposures_.
183
+ Exposures (`#exposures`) is set of exposed attributes, so that the view context can have the information needed to render a page.
184
+
185
+ ```ruby
186
+ class Show
187
+ include Lotus::Action
188
+
189
+ expose :article
190
+
191
+ def call(params)
192
+ @article = Article.find params[:id]
193
+ end
194
+ end
195
+
196
+ action = Show.new
197
+ action.call({ id: 23 })
198
+
199
+ assert_equal 23, action.article.id
200
+
201
+ puts action.exposures # => { article: <Article:0x007f965c1d0318 @id=23> }
202
+ ```
203
+
204
+ ### Callbacks
205
+
206
+ It offers powerful, inheritable callbacks chain which is executed before and/or after your `#call` method invocation:
207
+
208
+ ```ruby
209
+ class Show
210
+ include Lotus::Action
211
+
212
+ before :authenticate, :set_article
213
+
214
+ def call(params)
215
+ end
216
+
217
+ private
218
+ def authenticate
219
+ # ...
220
+ end
221
+
222
+ # `params` in the method signature is optional
223
+ def set_article(params)
224
+ @article = Article.find params[:id]
225
+ end
226
+ end
227
+ ```
228
+
229
+ Callbacks can also be expressed as anonymous lambdas:
230
+
231
+ ```ruby
232
+ class Show
233
+ include Lotus::Action
234
+
235
+ before { ... } # do some authentication stuff
236
+ before {|params| @article = Article.find params[:id] }
237
+
238
+ def call(params)
239
+ end
240
+ end
241
+ ```
242
+
243
+ ### Exceptions management
244
+
245
+ When an exception is raised, it automatically sets the HTTP status to [500](http://httpstatus.es/500):
246
+
247
+ ```ruby
248
+ class Show
249
+ include Lotus::Action
250
+
251
+ def call(params)
252
+ raise
253
+ end
254
+ end
255
+
256
+ action = Show.new
257
+ action.call({}) # => [500, {}, ["Internal Server Error"]]
258
+ ```
259
+
260
+ You can define how a specific raised exception should be transformed in an HTTP status.
261
+
262
+ ```ruby
263
+ class Show
264
+ include Lotus::Action
265
+ handle_exception RecordNotFound, 404
266
+
267
+ def call(params)
268
+ @article = Article.find params[:id]
269
+ end
270
+ end
271
+
272
+ action = Show.new
273
+ action.call({id: 'unknown'}) # => [404, {}, ["Not Found"]]
274
+ ```
275
+
276
+ Exception policies can be defined globally, **before** the controllers/actions
277
+ are loaded.
278
+
279
+ ```ruby
280
+ Lotus::Controller.handled_exceptions = { RecordNotFound => 404 }
281
+
282
+ class Show
283
+ include Lotus::Action
284
+
285
+ def call(params)
286
+ @article = Article.find params[:id]
287
+ end
288
+ end
289
+
290
+ action = Show.new
291
+ action.call({id: 'unknown'}) # => [404, {}, ["Not Found"]]
292
+ ```
293
+
294
+ ### Throwable HTTP statuses
295
+
296
+ When [#throw](http://ruby-doc.org/core-2.1.0/Kernel.html#method-i-throw) is used with a valid HTTP code, it stops the execution and sets the proper status and body for the response:
297
+
298
+ ```ruby
299
+ class Show
300
+ include Lotus::Action
301
+
302
+ before :authenticate!
303
+
304
+ def call(params)
305
+ # ...
306
+ end
307
+
308
+ private
309
+ def authenticate!
310
+ throw 401 unless authenticated?
311
+ end
312
+ end
313
+
314
+ action = Show.new
315
+ action.call({}) # => [401, {}, ["Unauthorized"]]
316
+ ```
317
+
318
+ ### Cookies
319
+
320
+ It offers convenient access to cookies.
321
+
322
+ They are read as an Hash from Rack env:
323
+
324
+ ```ruby
325
+ require 'lotus/controller'
326
+ require 'lotus/action/cookies'
327
+
328
+ class ReadCookiesFromRackEnv
329
+ include Lotus::Action
330
+ include Lotus::Action::Cookies
331
+
332
+ def call(params)
333
+ # ...
334
+ cookies[:foo] # => 'bar'
335
+ end
336
+ end
337
+
338
+ action = ReadCookiesFromRackEnv.new
339
+ action.call({'HTTP_COOKIE' => 'foo=bar'})
340
+ ```
341
+
342
+ They are set like an Hash:
343
+
344
+ ```ruby
345
+ require 'lotus/controller'
346
+ require 'lotus/action/cookies'
347
+
348
+ class SetCookies
349
+ include Lotus::Action
350
+ include Lotus::Action::Cookies
351
+
352
+ def call(params)
353
+ # ...
354
+ cookies[:foo] = 'bar'
355
+ end
356
+ end
357
+
358
+ action = SetCookies.new
359
+ action.call({}) # => [200, {'Set-Cookie' => 'foo=bar'}, '...']
360
+ ```
361
+
362
+ They are removed by setting their value to `nil`:
363
+
364
+ ```ruby
365
+ require 'lotus/controller'
366
+ require 'lotus/action/cookies'
367
+
368
+ class RemoveCookies
369
+ include Lotus::Action
370
+ include Lotus::Action::Cookies
371
+
372
+ def call(params)
373
+ # ...
374
+ cookies[:foo] = nil
375
+ end
376
+ end
377
+
378
+ action = SetCookies.new
379
+ action.call({}) # => [200, {'Set-Cookie' => "foo=; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"}, '...']
380
+ ```
381
+
382
+ ### Sessions
383
+
384
+ It has builtin support for Rack sessions:
385
+
386
+ ```ruby
387
+ require 'lotus/controller'
388
+ require 'lotus/action/session'
389
+
390
+ class ReadSessionFromRackEnv
391
+ include Lotus::Action
392
+ include Lotus::Action::Session
393
+
394
+ def call(params)
395
+ # ...
396
+ session[:age] # => '31'
397
+ end
398
+ end
399
+
400
+ action = ReadSessionFromRackEnv.new
401
+ action.call({ 'rack.session' => { 'age' => '31' }})
402
+ ```
403
+
404
+ Values can be set like an Hash:
405
+
406
+ ```ruby
407
+ require 'lotus/controller'
408
+ require 'lotus/action/session'
409
+
410
+ class SetSession
411
+ include Lotus::Action
412
+ include Lotus::Action::Session
413
+
414
+ def call(params)
415
+ # ...
416
+ session[:age] = 31
417
+ end
418
+ end
419
+
420
+ action = SetSession.new
421
+ action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."]
422
+ ```
423
+
424
+ Values can be removed like an Hash:
425
+
426
+ ```ruby
427
+ require 'lotus/controller'
428
+ require 'lotus/action/session'
429
+
430
+ class RemoveSession
431
+ include Lotus::Action
432
+ include Lotus::Action::Session
433
+
434
+ def call(params)
435
+ # ...
436
+ session[:age] = nil
437
+ end
438
+ end
439
+
440
+ action = RemoveSession.new
441
+ action.call({}) # => [200, {"Set-Cookie"=>"rack.session=..."}, "..."] it removes that value from the session
442
+ ```
443
+
444
+ While Lotus::Controller supports sessions natively, it's __session store agnostic__.
445
+ You have to specify the session store in your Rack middleware configuration (eg `config.ru`).
446
+
447
+ ```ruby
448
+ use Rack::Session::Cookie, secret: SecureRandom.hex(64)
449
+ run Show.new
450
+ ```
451
+
452
+ ### Redirect
453
+
454
+ If you need to redirect the client to another resource, use `#redirect_to`:
455
+
456
+ ```ruby
457
+ class Create
458
+ include Lotus::Action
459
+
460
+ def call(params)
461
+ # ...
462
+ redirect_to 'http://example.com/articles/23'
463
+ end
464
+ end
465
+
466
+ action = Create.new
467
+ action.call({ article: { title: 'Hello' }}) # => [302, {'Location' => '/articles/23'}, '']
468
+ ```
469
+
470
+ ### Mime types
471
+
472
+ Lotus::Action automatically sets the mime type, according to the request headers.
473
+ However, you can override this value:
474
+
475
+ ```ruby
476
+ class Show
477
+ include Lotus::Action
478
+
479
+ def call(params)
480
+ # ...
481
+ self.content_type = 'application/json'
482
+ end
483
+ end
484
+
485
+ action = Show.new
486
+ action.call({ id: 23 }) # => [200, {'Content-Type' => 'application/json'}, '...']
487
+ ```
488
+
489
+ You can restrict the accepted mime types:
490
+
491
+ ```ruby
492
+ class Show
493
+ include Lotus::Action
494
+ accept :html, :json
495
+
496
+ def call(params)
497
+ # ...
498
+ end
499
+ end
500
+
501
+ # When called with "\*/\*" => 200
502
+ # When called with "text/html" => 200
503
+ # When called with "application/json" => 200
504
+ # When called with "application/xml" => 406
505
+ ```
506
+
507
+ You can check if the requested mime type is accepted by the client.
508
+
509
+ ```ruby
510
+ class Show
511
+ include Lotus::Action
512
+
513
+ def call(params)
514
+ # ...
515
+ # @_env['HTTP_ACCEPT'] # => 'text/html,application/xhtml+xml,application/xml;q=0.9'
516
+
517
+ accept?('text/html') # => true
518
+ accept?('application/xml') # => true
519
+ accept?('application/json') # => false
520
+
521
+
522
+
523
+ # @_env['HTTP_ACCEPT'] # => '*/*'
524
+
525
+ accept?('text/html') # => true
526
+ accept?('application/xml') # => true
527
+ accept?('application/json') # => true
528
+ end
529
+ end
530
+ ```
531
+
532
+ ### No rendering, please
533
+
534
+ Lotus::Controller is designed to be a pure HTTP endpoint, rendering belongs to other layers of MVC.
535
+ You can set the body directly (see [response](#response)), or use [Lotus::View](https://github.com/lotus/view).
536
+
537
+ ### Controllers
538
+
539
+ A Controller is nothing more than a logical group for actions.
540
+
541
+ ```ruby
542
+ class ArticlesController
543
+ class Index
544
+ include Lotus::Action
545
+
546
+ # ...
547
+ end
548
+
549
+ class Show
550
+ include Lotus::Action
551
+
552
+ # ...
553
+ end
554
+ end
555
+ ```
556
+
557
+ Which is a bit verboses. Instead, just do:
558
+
559
+ ```ruby
560
+ class ArticlesController
561
+ include Lotus::Controller
562
+
563
+ action 'Index' do
564
+ # ...
565
+ end
566
+
567
+ action 'Show' do
568
+ # ...
569
+ end
570
+ end
571
+
572
+ ArticlesController::Index.new.call({})
573
+ ```
574
+
575
+ ## Lotus::Router integration
576
+
577
+ While Lotus::Router works great with this framework, Lotus::Controller doesn't depend from it.
578
+ You, as developer, are free to choose your own routing system.
579
+
580
+ But, if you use them together, the **only constraint is that an action must support _arity 0_ in its constructor**.
581
+ The following examples are valid constructors:
582
+
583
+ ```ruby
584
+ def initialize
585
+ end
586
+
587
+ def initialize(repository = Article)
588
+ end
589
+
590
+ def initialize(repository: Article)
591
+ end
592
+
593
+ def initialize(options = {})
594
+ end
595
+
596
+ def initialize(*args)
597
+ end
598
+ ```
599
+
600
+ __Please note that this is subject to change: we're working to remove this constraint.__
601
+
602
+ Lotus::Router supports lazy loading for controllers. While this policy can be a
603
+ convenient fallback, you should know that it's the slower option. **Be sure of
604
+ loading your controllers before you initialize the router.**
605
+
606
+
607
+ ## Rack integration
608
+
609
+ Lotus::Controller is compatible with Rack. However, it doesn't mount any middleware.
610
+ While a Lotus application's architecture is more web oriented, this framework is designed to build pure HTTP entpoints.
611
+
612
+ ## Thread safety
613
+
614
+ An Action is **mutable**. When used without Lotus::Router, be sure to instantiate an
615
+ action for each request.
616
+
617
+ ```ruby
618
+ # config.ru
619
+ require 'lotus/controller'
620
+
621
+ class Action
622
+ include Lotus::Action
623
+
624
+ def self.call(env)
625
+ new.call(env)
626
+ end
627
+
628
+ def call(params)
629
+ self.body = object_id.to_s
630
+ end
631
+ end
632
+
633
+ run Action
634
+ ```
635
+
636
+ ## Versioning
637
+
638
+ __Lotus::Controller__ uses [Semantic Versioning 2.0.0](http://semver.org)
22
639
 
23
640
  ## Contributing
24
641
 
25
- 1. Fork it ( http://github.com/<my-github-username>/lotus-controller/fork )
642
+ 1. Fork it
26
643
  2. Create your feature branch (`git checkout -b my-new-feature`)
27
644
  3. Commit your changes (`git commit -am 'Add some feature'`)
28
645
  4. Push to the branch (`git push origin my-new-feature`)
29
646
  5. Create new Pull Request
647
+
648
+ ## Copyright
649
+
650
+ Copyright 2014 Luca Guidi – Released under MIT License