lotus-controller 0.0.0 → 0.1.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 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