eldr 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.rspec +3 -0
- data/.rubocop.yml +28 -0
- data/.rubocop_todo.yml +15 -0
- data/.travis.yml +3 -0
- data/DUCK.md +15 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +89 -0
- data/LICENSE +20 -0
- data/README.md +876 -0
- data/Rakefile +9 -0
- data/TODOS +3 -0
- data/eldr.gemspec +36 -0
- data/examples/README.md +7 -0
- data/examples/action_objects.ru +28 -0
- data/examples/app.ru +75 -0
- data/examples/builder.ru +24 -0
- data/examples/custom_response.ru +23 -0
- data/examples/errors.ru +16 -0
- data/examples/hello_world.ru +8 -0
- data/examples/inheritance.ru +22 -0
- data/examples/multiple_apps.ru +30 -0
- data/examples/rails_style_routing.ru +14 -0
- data/examples/rendering_templates.ru +38 -0
- data/examples/views/cats.slim +1 -0
- data/lib/eldr/app.rb +146 -0
- data/lib/eldr/builder.rb +60 -0
- data/lib/eldr/configuration.rb +37 -0
- data/lib/eldr/matcher.rb +36 -0
- data/lib/eldr/recognizer.rb +35 -0
- data/lib/eldr/route.rb +92 -0
- data/lib/eldr/version.rb +3 -0
- data/lib/eldr.rb +1 -0
- data/spec/app_spec.rb +194 -0
- data/spec/builder_spec.rb +11 -0
- data/spec/configuration_spec.rb +29 -0
- data/spec/examples/action_objects_spec.rb +18 -0
- data/spec/examples/builder_spec.rb +16 -0
- data/spec/examples/custom_response_spec.rb +17 -0
- data/spec/examples/errors_spec.rb +18 -0
- data/spec/examples/example_app_spec.rb +98 -0
- data/spec/examples/hello_world_spec.rb +17 -0
- data/spec/examples/inheritance_spec.rb +23 -0
- data/spec/examples/multiple_apps_spec.rb +31 -0
- data/spec/examples/rails_style_routing_spec.rb +17 -0
- data/spec/examples/rendering_templates_spec.rb +26 -0
- data/spec/matcher_spec.rb +21 -0
- data/spec/readme_definitions.yml +28 -0
- data/spec/readme_spec.rb +117 -0
- data/spec/recognizer_spec.rb +32 -0
- data/spec/route_spec.rb +131 -0
- data/spec/spec_helper.rb +14 -0
- metadata +252 -0
data/README.md
ADDED
@@ -0,0 +1,876 @@
|
|
1
|
+
# Eldr [![Build Status](https://travis-ci.org/eldr-rb/eldr.svg)](https://travis-ci.org/eldr-rb/eldr) [![Code Climate](https://codeclimate.com/github/eldr-rb/eldr/badges/gpa.svg)](https://codeclimate.com/github/eldr-rb/eldr) [![Coverage Status](https://coveralls.io/repos/eldr-rb/eldr/badge.svg?branch=master)](https://coveralls.io/r/eldr-rb/eldr?branch=master) [![Dependency Status](https://gemnasium.com/eldr-rb/eldr.svg)](https://gemnasium.com/eldr-rb/eldr) [![Inline docs](https://inch-ci.org/github/eldr-rb/eldr.svg?branch=master)](http://inch-ci.org/github/eldr-rb/eldr) [![Gratipay](https://img.shields.io/gratipay/k2052.svg)](https://www.gratipay.com/k2052/)
|
2
|
+
|
3
|
+
Eldr is a minimal ruby framework that doesn't hide the rack. It aims to be lightweight, simple, modular and above all, clear. Eldr is a ruby framework without all the magic.
|
4
|
+
|
5
|
+
Eldr apps are rack apps like this:
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class App < Eldr::App
|
9
|
+
get '/posts' do |params|
|
10
|
+
Rack::Response.new "posts", 200
|
11
|
+
end
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
Since they are rack apps you can boot them like this:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
run App
|
19
|
+
```
|
20
|
+
|
21
|
+
And when you want to combine them you can do this:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
class Posts < Eldr::App
|
25
|
+
get '/' do |params|
|
26
|
+
Rack::Response.new "posts", 200
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
class Tasks < Eldr::App
|
31
|
+
get '/' do |params|
|
32
|
+
Rack::Response.new "tasks", 200
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
map '/posts' do
|
37
|
+
Posts.new
|
38
|
+
end
|
39
|
+
|
40
|
+
map '/tasks' do
|
41
|
+
run Tasks.new
|
42
|
+
end
|
43
|
+
```
|
44
|
+
|
45
|
+
<!-- START doctoc generated TOC please keep comment here to allow auto update -->
|
46
|
+
<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
|
47
|
+
## Table of Contents
|
48
|
+
|
49
|
+
- [Eldr](#eldr)
|
50
|
+
- [Features](#features)
|
51
|
+
- [Installation and Usage](#installation-and-usage)
|
52
|
+
- [Quickstart Guides](#quickstart-guides)
|
53
|
+
- [Hello World](#hello-world)
|
54
|
+
- [Rendering a Template](#rendering-a-template)
|
55
|
+
- [Before/After Filters](#beforeafter-filters)
|
56
|
+
- [Helpers](#helpers)
|
57
|
+
- [Rails Style Routing](#rails-style-routing)
|
58
|
+
- [Rails Style Responses](#rails-style-responses)
|
59
|
+
- [Errors](#errors)
|
60
|
+
- [Error Catching](#error-catching)
|
61
|
+
- [Rails Style Requests](#rails-style-requests)
|
62
|
+
- [Conditions](#conditions)
|
63
|
+
- [Access Control](#access-control)
|
64
|
+
- [Inheritance](#inheritance)
|
65
|
+
- [Extensions](#extensions)
|
66
|
+
- [Extending using Ruby patterns](#extending-using-ruby-patterns)
|
67
|
+
- [Extending using Rack](#extending-using-rack)
|
68
|
+
- [Using Rack inside Eldr](#using-rack-inside-eldr)
|
69
|
+
- [Redirecting Things](#redirecting-things)
|
70
|
+
- [Route Handlers: The Power of Action Objects](#route-handlers-the-power-of-action-objects)
|
71
|
+
- [Testing Eldr Apps](#testing-eldr-apps)
|
72
|
+
- [Performance](#performance)
|
73
|
+
- [Help/Resources](#helpresources)
|
74
|
+
- [Contributing](#contributing)
|
75
|
+
- [License](#license)
|
76
|
+
|
77
|
+
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
|
78
|
+
|
79
|
+
## Features
|
80
|
+
|
81
|
+
- **Small** Eldr's core is under 500 lines; you can learn it in an afternoon.
|
82
|
+
- **Flexible** Build your apps how you want to and with what you want.
|
83
|
+
- **Modular** Eldr's views, response helpers, sessions etc; are all independent gems.
|
84
|
+
- **No Magic** Eldr encourages you to build apps that you actually understand.
|
85
|
+
- **Fast** Eldr is among the fastest ruby microframeworks and it is getting faster.
|
86
|
+
- **Powerful Router** Powered by [Mustermann](https://github.com/rkh/mustermann) -- you can use everything from Sinatra to Rails style routing.
|
87
|
+
- **Flexible** That router? Is also just a Rack app. You can swap it out just like everything else in Eldr.
|
88
|
+
- **Extensible** with standard ruby classes, modules, and patterns. In Eldr there are no mystical helper blocks.
|
89
|
+
|
90
|
+
## Installation and Usage
|
91
|
+
|
92
|
+
To install Eldr add the following to your gemfile:
|
93
|
+
|
94
|
+
```ruby
|
95
|
+
gem 'eldr'
|
96
|
+
# or if you want to use the master branch:
|
97
|
+
# gem 'eldr', github: 'eldr-rb/eldr'
|
98
|
+
```
|
99
|
+
|
100
|
+
Then run bundler:
|
101
|
+
|
102
|
+
```sh
|
103
|
+
$ bundle
|
104
|
+
```
|
105
|
+
|
106
|
+
To use it you need to create a rackup file. Add the following to a config.ru file:
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
class HelloWorld < Edlr::App
|
110
|
+
get '/', proc { [200, {'Content-Type' => 'txt'}, ['Hello World!']]}
|
111
|
+
end
|
112
|
+
|
113
|
+
run HelloWorld
|
114
|
+
```
|
115
|
+
|
116
|
+
Route handlers are anything that respond to call and return a valid Rack response. All the http verbs you expect are available:
|
117
|
+
|
118
|
+
```ruby
|
119
|
+
get '/', proc { [200, {'Content-Type' => 'txt'}, ['Hello World!']]}
|
120
|
+
post '/', proc { [201, {'Content-Type' => 'txt'}, ['Hello World!']]}
|
121
|
+
```
|
122
|
+
|
123
|
+
etc...
|
124
|
+
|
125
|
+
For further usage examples checkout the [https://github.com/eldr-rb/eldr/tree/master/examples](examples folder)
|
126
|
+
|
127
|
+
I have already built and released extensions for many common tasks:
|
128
|
+
|
129
|
+
- [eldr-sessions](https://github.com/eldr-rb/eldr-sessions): session helpers like `signed_in?` and `current_user?`
|
130
|
+
- [eldr-rendering](https://github.com/eldr-rb/eldr-rendering): a `render` helper for templating.
|
131
|
+
- [eldr-assets](https://github.com/eldr-rb/eldr-assets): asset tag helpers like `js('jquery', 'app')`, `css('app')` etc
|
132
|
+
- [eldr-responders](https://github.com/eldr-rb/eldr-responders): rails-responder like helpers
|
133
|
+
- [eldr-action](https://github.com/eldr-rb/eldr-action): Action Objects
|
134
|
+
- [eldr-service](https://github.com/eldr-rb/eldr-service): Action Objects for external services.
|
135
|
+
- [eldr-cascade](https://github.com/eldr-rb/eldr-cascade): A fork of Rack::Cascade that plays well with Rack::Response and eldr-action.
|
136
|
+
|
137
|
+
## Quickstart Guides
|
138
|
+
|
139
|
+
### Hello World
|
140
|
+
|
141
|
+
Start by adding the following to your gemfile:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
gem 'edlr'
|
145
|
+
```
|
146
|
+
|
147
|
+
Then run bundler:
|
148
|
+
|
149
|
+
```
|
150
|
+
$ bundle install
|
151
|
+
```
|
152
|
+
|
153
|
+
Now create a new file called app.rb with the following contents:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
class App < Eldr::App
|
157
|
+
get '/' do |params|
|
158
|
+
Rack::Response.new "Hello World!", 200
|
159
|
+
end
|
160
|
+
end
|
161
|
+
```
|
162
|
+
|
163
|
+
This defines a new App with a root route that returns "Hello World!". To make use of this app we need to add a rackup file. In the root of your project create a new file called config.ru:
|
164
|
+
|
165
|
+
```ruby
|
166
|
+
require_relative 'app'
|
167
|
+
run App
|
168
|
+
```
|
169
|
+
|
170
|
+
Now boot it using rackup:
|
171
|
+
|
172
|
+
```sh
|
173
|
+
$ rackup
|
174
|
+
```
|
175
|
+
|
176
|
+
When you visit http:///localhost:9292 in your browser you should see "Hello World!"
|
177
|
+
|
178
|
+
### Rendering a Template
|
179
|
+
|
180
|
+
Eldr provides no render helper in it's core but it is easy to define your own.
|
181
|
+
One can use Tilt as the templating library (with your engine of choice) and then create some helper methods to handle finding the template and rendering it.
|
182
|
+
|
183
|
+
Create a module with the following:
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
module RenderingHelpers
|
187
|
+
def render(path, resp_code=200)
|
188
|
+
Rack::Response.new Tilt.new(find_template(path)).render(self), resp_code
|
189
|
+
end
|
190
|
+
|
191
|
+
def find_template(path)
|
192
|
+
template = File.join('views', path)
|
193
|
+
raise NotFoundError unless File.exists? template
|
194
|
+
template
|
195
|
+
end
|
196
|
+
end
|
197
|
+
```
|
198
|
+
|
199
|
+
This takes a template, finds the template and then passes it off to tilt for handling.
|
200
|
+
If the template does not exist it raises a NotFoundError.
|
201
|
+
|
202
|
+
We can use these helpers by including them in our App:
|
203
|
+
|
204
|
+
```ruby
|
205
|
+
class App
|
206
|
+
include RenderingHelpers
|
207
|
+
|
208
|
+
get '/cats' do
|
209
|
+
render 'cats.slim'
|
210
|
+
end
|
211
|
+
end
|
212
|
+
```
|
213
|
+
|
214
|
+
Using it, is as simple as including it!
|
215
|
+
|
216
|
+
*Checkout*: [eldr-rendering](https://github.com/eldr-rb/eldr-rendering) for some pre-made rendering helpers.
|
217
|
+
|
218
|
+
### Before/After Filters
|
219
|
+
|
220
|
+
Eldr comes with support for before/after filters. You can add a filter like this:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
class App < Eldr::App
|
224
|
+
before do
|
225
|
+
@message = 'Hello World!'
|
226
|
+
end
|
227
|
+
|
228
|
+
after do
|
229
|
+
puts 'after'
|
230
|
+
end
|
231
|
+
|
232
|
+
get '/', proc { [200, {}, [@message]] }
|
233
|
+
end
|
234
|
+
```
|
235
|
+
|
236
|
+
Filters can be limited to specific routes by giving your route a name:
|
237
|
+
|
238
|
+
```ruby
|
239
|
+
class App < Eldr::App
|
240
|
+
before(:bob) do
|
241
|
+
@message = 'Hello World!'
|
242
|
+
end
|
243
|
+
|
244
|
+
after(:bob) do
|
245
|
+
puts 'after'
|
246
|
+
end
|
247
|
+
|
248
|
+
get '/', name: :bob, proc { [200, {}, [@message]] }
|
249
|
+
end
|
250
|
+
```
|
251
|
+
|
252
|
+
### Helpers
|
253
|
+
|
254
|
+
In Eldr, helpers are not mystical elements, they are plain ruby modules:
|
255
|
+
|
256
|
+
```ruby
|
257
|
+
module Helpers
|
258
|
+
end
|
259
|
+
```
|
260
|
+
|
261
|
+
If you need registration callbacks then you can make use of ruby's own included callback:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
module Helpers
|
265
|
+
def included(klass)
|
266
|
+
end
|
267
|
+
end
|
268
|
+
```
|
269
|
+
|
270
|
+
If you need to define these methods directly on the app class, something like _helpers do_, you can do the following:
|
271
|
+
|
272
|
+
```ruby
|
273
|
+
class App
|
274
|
+
def helper_method
|
275
|
+
end
|
276
|
+
end
|
277
|
+
```
|
278
|
+
|
279
|
+
This will put your helper method on every instance of your app's class and make it available to any templates.
|
280
|
+
|
281
|
+
### Rails Style Routing
|
282
|
+
|
283
|
+
In the rails world routes are methods on controller instances:
|
284
|
+
|
285
|
+
```ruby
|
286
|
+
class Cats
|
287
|
+
def index
|
288
|
+
# stuff here
|
289
|
+
end
|
290
|
+
end
|
291
|
+
```
|
292
|
+
|
293
|
+
Then in a separate router object we map our routes to these action methods.
|
294
|
+
|
295
|
+
```ruby
|
296
|
+
get '/cats', to: 'cats#index'
|
297
|
+
```
|
298
|
+
|
299
|
+
This is easy to do with Eldr; because Eldr apps are Rack apps, we can use one as a dispatcher/router and then another as our controller.
|
300
|
+
|
301
|
+
```ruby
|
302
|
+
class Cats
|
303
|
+
def index
|
304
|
+
Rack::Response.new "Hello From all the Cats!"
|
305
|
+
end
|
306
|
+
end
|
307
|
+
```
|
308
|
+
|
309
|
+
Then run it:
|
310
|
+
|
311
|
+
```ruby
|
312
|
+
run Router.new do
|
313
|
+
get '/cats', to: 'cats#index'
|
314
|
+
end
|
315
|
+
```
|
316
|
+
|
317
|
+
We could add a lot more abstraction but this is simple enough and accomplishes our goals. Eldr encourages you to _build up_ abstractions as you go.
|
318
|
+
|
319
|
+
Pre-building flexibility and accounting for things you don't need to do yet is unnecessary work. You should build apps that you can _see into_ and actually understand, no magic.
|
320
|
+
|
321
|
+
### Rails Style Responses
|
322
|
+
|
323
|
+
The Rails world has a tremendous amount of conventions and abstraction for dealing with responses. They range from complex to simple, from format helpers to full fledged REST abstractions. At their core, they all take information from Rack::Request and turn it into a valid Rack response -- and a valid rack response is just an array.
|
324
|
+
|
325
|
+
Once we realize this, replicating rails becomes just a matter of patterns.
|
326
|
+
|
327
|
+
Eldr routes must return a valid rack response, which is an array -- actually its just something that can act like an array. We can return an object that responds to :to_a and Rack will be just as happy.
|
328
|
+
|
329
|
+
Let's create a object that holds a response string, response headers, and a status.
|
330
|
+
|
331
|
+
```ruby
|
332
|
+
class Response
|
333
|
+
attr_accessor :status, :headers, :body
|
334
|
+
|
335
|
+
def initialize(body, status=200, headers={})
|
336
|
+
@body, @status, @headers = body, status, headers
|
337
|
+
end
|
338
|
+
end
|
339
|
+
```
|
340
|
+
|
341
|
+
Now add a to_a method:
|
342
|
+
|
343
|
+
```ruby
|
344
|
+
def to_a
|
345
|
+
[@status, @headers, [@body]]
|
346
|
+
end
|
347
|
+
alias_method :to_ary, :to_a # this makes ruby aware the object can act like an array
|
348
|
+
```
|
349
|
+
|
350
|
+
To use this we just return a new instance of it as our response:
|
351
|
+
|
352
|
+
```ruby
|
353
|
+
Eldr::App.new do
|
354
|
+
get '/' do
|
355
|
+
Response.new("Hello World!")
|
356
|
+
end
|
357
|
+
end
|
358
|
+
```
|
359
|
+
|
360
|
+
This simple pattern is enough to build all sorts of powerful abstractions.
|
361
|
+
|
362
|
+
### Errors
|
363
|
+
|
364
|
+
You can `raise` an error with any class that inherits from StandardError and responds to call. This is useful in before filters, when you want to halt a route from executing.
|
365
|
+
|
366
|
+
An error class looks like this:
|
367
|
+
|
368
|
+
```ruby
|
369
|
+
class ErrorResponse < StandardErrro
|
370
|
+
def call(env)
|
371
|
+
Rack::Response.new message, 500
|
372
|
+
end
|
373
|
+
end
|
374
|
+
```
|
375
|
+
|
376
|
+
And you can use it like a standard ruby error class:
|
377
|
+
|
378
|
+
```ruby
|
379
|
+
app = Edlr::App.new do
|
380
|
+
get '/' do
|
381
|
+
raise ErrorResponse, "Bad Data"
|
382
|
+
end
|
383
|
+
end
|
384
|
+
|
385
|
+
run app
|
386
|
+
```
|
387
|
+
|
388
|
+
### Error Catching
|
389
|
+
|
390
|
+
Eldr will NOT catch all errors. In a production setting you will need to use middleware to make certain nothing ever explodes at your user. Something like [rack-robustness](https://github.com/blambeau/rack-robustness) will work fine:
|
391
|
+
|
392
|
+
```ruby
|
393
|
+
class App < Edlr::App
|
394
|
+
use Rack::Robustness do |g|
|
395
|
+
g.on(ArgumentError){|ex| 400 }
|
396
|
+
g.on(SecurityError){|ex| 403 }
|
397
|
+
|
398
|
+
g.content_type 'text/plain'
|
399
|
+
|
400
|
+
g.body{|ex|
|
401
|
+
ex.message
|
402
|
+
}
|
403
|
+
|
404
|
+
g.ensure(true){|ex|
|
405
|
+
env['rack.errors'].write(ex.message)
|
406
|
+
}
|
407
|
+
end
|
408
|
+
end
|
409
|
+
|
410
|
+
run App
|
411
|
+
```
|
412
|
+
|
413
|
+
### Rails Style Requests
|
414
|
+
|
415
|
+
Rails provides all sorts of ways to digest the data passed to it by a client. At their core they all operate on Rack's env object. They take Rack's env object and pass it off to a wrapper.
|
416
|
+
|
417
|
+
The wrapper modifies the request _only_ on the env object, this means at any point we can duplicate the state of the wrapper by passing it the env; and any objects that need parsed request data (e.g params) will know to find it in the env object.
|
418
|
+
|
419
|
+
The most typical way rails helps us with requests is through parameter parsing and validation. If we of params a just a hash, then validating, error handling on them etc is simple, we can use all the tools we are already used to.
|
420
|
+
|
421
|
+
Lets start by getting our parameters into a hash. To do this we pass env into Rack::Request; a wrapper object that parses the query string and injects the results into @env["rack.request.query_hash"]:
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
class App < Eldr::App
|
425
|
+
post '/cats' do |env|
|
426
|
+
request = Rack::Request.new(env)
|
427
|
+
params = request.GET # we could also access it from env["rack.request.query_hash"]
|
428
|
+
end
|
429
|
+
end
|
430
|
+
```
|
431
|
+
|
432
|
+
Now we will need an object to _validate_ the parameters. We can use Virtus and ActiveModel::Validations for this:
|
433
|
+
|
434
|
+
```ruby
|
435
|
+
class CatParams
|
436
|
+
include Virtus.model
|
437
|
+
include ActiveModel::Validations
|
438
|
+
|
439
|
+
attribute :name, String
|
440
|
+
attribute :age, Integer
|
441
|
+
attribute :human_kils, Integer
|
442
|
+
|
443
|
+
validates :name, :age, :human_kills, presence: true
|
444
|
+
end
|
445
|
+
```
|
446
|
+
|
447
|
+
To _validate_ a request we need to instantiate CatParams:
|
448
|
+
|
449
|
+
```ruby
|
450
|
+
class InvalidParams < Edlr::ResponseError
|
451
|
+
def status
|
452
|
+
400
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
class App < Eldr::App
|
457
|
+
post '/cats' do |env|
|
458
|
+
request = Rack::Request.new(env)
|
459
|
+
params = CatParams.new(request.GET)
|
460
|
+
|
461
|
+
raise InvalidParams.new(errors) unless params.valid?
|
462
|
+
|
463
|
+
cat = Cat.create(params.attributes)
|
464
|
+
Rack::Response.new(cat.to_json)
|
465
|
+
end
|
466
|
+
end
|
467
|
+
```
|
468
|
+
|
469
|
+
If our parameters are invalid we raise an InvalidParams response (Eldr has error handling). If they are valid, then we create our Cat and return it as JSON.
|
470
|
+
|
471
|
+
### Conditions
|
472
|
+
|
473
|
+
Most microframeworks provide a DSL for route conditions. This needlesly complicates a framework with multiple ways to do things. Do we check for _x_ in our conditions or do we check for it in the route's handler?
|
474
|
+
|
475
|
+
Eldr's strives for clarity. There is only one place to place logic for a route, in the route's handler.
|
476
|
+
|
477
|
+
If you want a route to only be executed under certain conditions you check those conditions in the handler and then `throw :pass` if conditions match/do not match:
|
478
|
+
|
479
|
+
```ruby
|
480
|
+
get '/' do
|
481
|
+
throw :pass if params['agent'] == 'secret'
|
482
|
+
# respond here
|
483
|
+
end
|
484
|
+
|
485
|
+
# Executed only if we pass from the first route
|
486
|
+
get '/' do
|
487
|
+
end
|
488
|
+
```
|
489
|
+
|
490
|
+
### Access Control
|
491
|
+
|
492
|
+
Originally Eldr had an access control DSL that looked like this:
|
493
|
+
|
494
|
+
```ruby
|
495
|
+
access_control do
|
496
|
+
allow :create, :registered, :admin
|
497
|
+
end
|
498
|
+
```
|
499
|
+
|
500
|
+
The DSL pulled a route's name and it's roles into a before filter, then checked them against the current_user's roles. The filter itself was five lines.
|
501
|
+
|
502
|
+
I soon realized that this was redundant abstraction. The DSL didn't save me any coding, it merely gave the code pretty words. I was sacrificing clarity for poetry.
|
503
|
+
|
504
|
+
To protect a route you need a before filter that checks the user for roles. It can look like this:
|
505
|
+
|
506
|
+
```ruby
|
507
|
+
before :create, :edit do
|
508
|
+
unless current_user.has_role? :admin
|
509
|
+
raise NotAuthorized, "You are not authorized to do this!"
|
510
|
+
end
|
511
|
+
end
|
512
|
+
```
|
513
|
+
|
514
|
+
It is just as short as any DSL and ambiguity free. Any ruby developer can guess what
|
515
|
+
it means without having to know your framework's secret language.
|
516
|
+
|
517
|
+
### Inheritance
|
518
|
+
|
519
|
+
In many frameworks you are encouraged to wrap your controllers in a central app.
|
520
|
+
You build a main app that handles routing, scoping, sharing configuration and middleware etc; often controllers are merely blocks executed in the context of this app. This allows you to build DRY controllers but at the cost of a large central app.
|
521
|
+
|
522
|
+
In Eldr we reverse this model and encourage you to build up your controllers through inheritance. Eldr apps can inherit middleware and configuration from parent apps.
|
523
|
+
This allows you to define a base app and allow all your controllers to share the same configuration.
|
524
|
+
|
525
|
+
For example:
|
526
|
+
|
527
|
+
```ruby
|
528
|
+
class SimpleMiddleware
|
529
|
+
def initialize(app)
|
530
|
+
@app = app
|
531
|
+
end
|
532
|
+
|
533
|
+
def call(env)
|
534
|
+
env['eldr.simple_counter'] ||= 0
|
535
|
+
env['eldr.simple_counter'] += 1
|
536
|
+
@app.call(env)
|
537
|
+
end
|
538
|
+
end
|
539
|
+
|
540
|
+
class BaseApp < Eldr::App
|
541
|
+
use SimpleCounterMiddleware
|
542
|
+
set :bob, 'what about him?'
|
543
|
+
end
|
544
|
+
|
545
|
+
class InheritedApp < BaseApp
|
546
|
+
get '/', proc { [200, {}, [env['eldr.simple_counter']]]}
|
547
|
+
end
|
548
|
+
|
549
|
+
run InheritedApp
|
550
|
+
```
|
551
|
+
|
552
|
+
### Extensions
|
553
|
+
|
554
|
+
Extending a framework is a mystical process full of esoteric patterns and confusing APIs. Engines, helper blocks, plugin apis, and if you are lucky yaml configuration.
|
555
|
+
|
556
|
+
You can extend Eldr in two ways;
|
557
|
+
|
558
|
+
1. Through standard Ruby patterns
|
559
|
+
2. Through standard Rack patterns
|
560
|
+
|
561
|
+
#### Extending using Ruby patterns
|
562
|
+
|
563
|
+
Ruby patterns are something you are already intimately familiar with. Include, extend, ducks, blocks, procs etc are something you know like the front of your MacBook (btw you should _really_ clean that smudge). Eldr is a DSL you already know how to speak.
|
564
|
+
|
565
|
+
I have designed Edlr to work well with common ruby patterns; you can include it, extend it, pass it blocks etc, and it wont blow up. If it does blow up its a bug.
|
566
|
+
|
567
|
+
You can add methods to your app (i.e helpers) by including them:
|
568
|
+
|
569
|
+
```ruby
|
570
|
+
class App < Eldr::App
|
571
|
+
include Helpers
|
572
|
+
end
|
573
|
+
```
|
574
|
+
|
575
|
+
If you want to use an app as a block you can:
|
576
|
+
|
577
|
+
```ruby
|
578
|
+
run App.new do
|
579
|
+
get '/' { Rack::Response.new "Hello World" }
|
580
|
+
end
|
581
|
+
```
|
582
|
+
|
583
|
+
Flexibility in Eldr is not accomplished through abstraction but through clarity. You can make Eldr do what you want it to because you will understand it not because it provides every feature.
|
584
|
+
|
585
|
+
#### Extending using Rack
|
586
|
+
|
587
|
+
Rails has a powerful code reuse model. You can generalize routes, controllers, even assets and views, then share them. Authentication/Authorization, shopping carts, social engines, admin panels, all abound in the rails world. This reuse of code allows one to build complex large apps that once took months, in a matter of minutes.
|
588
|
+
|
589
|
+
In Eldr you re-use applications the Rack way. You will architect your app so it can _be mounted_ as a Rack app, then an end-developer can mount it or extend it using Rack tools.
|
590
|
+
|
591
|
+
For example, imagine a user registration/authentication app, something like Devise.
|
592
|
+
|
593
|
+
First we create our app class:
|
594
|
+
|
595
|
+
```ruby
|
596
|
+
class EldrWise
|
597
|
+
###
|
598
|
+
# Registrations
|
599
|
+
###
|
600
|
+
|
601
|
+
# Register a new user
|
602
|
+
post '/user' do
|
603
|
+
# ... user creation stuff
|
604
|
+
redirect_to "/users/#{user.id}"
|
605
|
+
end
|
606
|
+
|
607
|
+
# ... more code
|
608
|
+
|
609
|
+
###
|
610
|
+
# Users
|
611
|
+
###
|
612
|
+
get '/users/:id' do
|
613
|
+
render 'users/show'
|
614
|
+
end
|
615
|
+
end
|
616
|
+
```
|
617
|
+
|
618
|
+
We would probably want to break this up into separate controllers:
|
619
|
+
|
620
|
+
```ruby
|
621
|
+
class EldrWise::Registrations
|
622
|
+
# ... registration routes
|
623
|
+
end
|
624
|
+
|
625
|
+
class EldrWise::Users
|
626
|
+
# ... user routes
|
627
|
+
end
|
628
|
+
|
629
|
+
module EldrWise
|
630
|
+
map '/registration' do
|
631
|
+
Registrations
|
632
|
+
end
|
633
|
+
|
634
|
+
map '/users' do
|
635
|
+
Users
|
636
|
+
end
|
637
|
+
end
|
638
|
+
```
|
639
|
+
|
640
|
+
Then an end user can either mount the entire thing:
|
641
|
+
|
642
|
+
```ruby
|
643
|
+
App.extend EldrWise
|
644
|
+
run App
|
645
|
+
```
|
646
|
+
|
647
|
+
Or just a part:
|
648
|
+
|
649
|
+
```ruby
|
650
|
+
map '/users' do
|
651
|
+
EldrWise::Users
|
652
|
+
end
|
653
|
+
run App
|
654
|
+
```
|
655
|
+
|
656
|
+
To customize it they can either extend it:
|
657
|
+
|
658
|
+
```ruby
|
659
|
+
class UsersController < EldrWise::Users
|
660
|
+
end
|
661
|
+
|
662
|
+
map '/users' do
|
663
|
+
UsersController
|
664
|
+
end
|
665
|
+
```
|
666
|
+
|
667
|
+
If we wan to define all our controllers on the root we can use [eldr-cascade](https://github.com/eldr-rb/eldr-cascade). We define the routes we want to override, Cascade will get a 404 on the ones we didn't, then call the next app until it gets a response:
|
668
|
+
|
669
|
+
```ruby
|
670
|
+
class App
|
671
|
+
# override the users post route but nothing else
|
672
|
+
post '/users' do
|
673
|
+
end
|
674
|
+
end
|
675
|
+
run Eldr::Cascade.new[App, EldrWise]
|
676
|
+
```
|
677
|
+
|
678
|
+
Thinking about Rack when you construct your Eldr apps will make them flexible and easy to reuse.
|
679
|
+
|
680
|
+
### Using Rack inside Eldr
|
681
|
+
|
682
|
+
Eldr apps are an instance of Rack::Builder; this means we can use any middleware or other Rack apps (including other Eldr apps) inside an app. To use Rack::Builder features you call the methods on the class.
|
683
|
+
|
684
|
+
For example, to use Rack session cookies we do:
|
685
|
+
|
686
|
+
```ruby
|
687
|
+
class App < Eldr::App
|
688
|
+
use Rack::Session::Cookie
|
689
|
+
end
|
690
|
+
```
|
691
|
+
|
692
|
+
If we wanted to mount other Eldr apps/controllers in our app we would do the following:
|
693
|
+
|
694
|
+
```ruby
|
695
|
+
class App < Eldr::App
|
696
|
+
map '/users' do
|
697
|
+
UsersController
|
698
|
+
end
|
699
|
+
end
|
700
|
+
```
|
701
|
+
|
702
|
+
Because rack builder itself can take rack builder instances we can run our App in config.ru just like we expect:
|
703
|
+
|
704
|
+
```ruby
|
705
|
+
class App < Eldr::App
|
706
|
+
use Rack::Session::Cookie
|
707
|
+
end
|
708
|
+
|
709
|
+
run App
|
710
|
+
```
|
711
|
+
|
712
|
+
### Redirecting Things
|
713
|
+
|
714
|
+
Redirects are an _enormously_ complex subject involving thousands of lines of helper code. Just kidding, they are just a status code and a new path. We can create a redirection helper in 5 lines:
|
715
|
+
|
716
|
+
```ruby
|
717
|
+
module Helpers
|
718
|
+
def redirect(path, message)
|
719
|
+
[302, {'location' => path}, [message]]
|
720
|
+
end
|
721
|
+
end
|
722
|
+
```
|
723
|
+
|
724
|
+
### Route Handlers: The Power of Action Objects
|
725
|
+
|
726
|
+
In Edlr (just like in Rack) route handlers are things that respond to call. This means we can use everthing from procs, to blocks, to instances of a class.
|
727
|
+
|
728
|
+
If we wanted to we could do the following:
|
729
|
+
|
730
|
+
```ruby
|
731
|
+
class Handler
|
732
|
+
def call(env)
|
733
|
+
Rack::Response.new "Hi there Dave!"
|
734
|
+
end
|
735
|
+
end
|
736
|
+
|
737
|
+
class App
|
738
|
+
get '/', Handler.new
|
739
|
+
end
|
740
|
+
```
|
741
|
+
|
742
|
+
This isn't as silly as it might seem. Its a common and powerful pattern that can clean up
|
743
|
+
complex controllers.
|
744
|
+
|
745
|
+
Imagine we have a complex show action that takes up hundreds of lines. Our first inclination will be to split this up into different methods on our controller. This would make our show action a lot shorter; but we would have logic for our show action spread across our controller. Our other actions don't need to be able to access the show action's logic. It would be better to have all the logic for the show action in one object.
|
746
|
+
|
747
|
+
We can do this by writing our show action as an Action Object. Our action's logic will be contained entirely in this object.
|
748
|
+
|
749
|
+
Action objects give us the freedom to instantiatie them, inject things, pass them around, test and generally just engage in whatever sociopathic whimsys we might want to do to them.
|
750
|
+
|
751
|
+
Let's take a look at that show action:
|
752
|
+
|
753
|
+
```ruby
|
754
|
+
class Show
|
755
|
+
attr_accessor :env
|
756
|
+
|
757
|
+
def helper_logic
|
758
|
+
# do things here
|
759
|
+
end
|
760
|
+
|
761
|
+
def params
|
762
|
+
env['eldr.params']
|
763
|
+
end
|
764
|
+
|
765
|
+
def call(env)
|
766
|
+
@env = env
|
767
|
+
|
768
|
+
helper_logic
|
769
|
+
# @cat = Cat.find params[:id]
|
770
|
+
Rack::Response.new "Found cat named #{params['name'].capitalize}!"
|
771
|
+
end
|
772
|
+
end
|
773
|
+
|
774
|
+
class CatsController
|
775
|
+
get '/cats/:name', Show.new
|
776
|
+
end
|
777
|
+
|
778
|
+
run CatsController
|
779
|
+
```
|
780
|
+
|
781
|
+
Once you start using this pattern you cant stop. Like nutella, you start putting it on everything, trying it on absurd things you know are wrong -- just in case it might work well.
|
782
|
+
|
783
|
+
Don't overuse action objects, but remember they are there when you want deliciousness.
|
784
|
+
|
785
|
+
### Testing Eldr Apps
|
786
|
+
|
787
|
+
You are ready to set out on your own but you are afraid of breaking all the things. You need some safety checks to keep you from hurting yourself or your apps. Testing Eldr apps is the same as testing rack apps.
|
788
|
+
|
789
|
+
In your spec helper include rack-test. With rspec that would look like this:
|
790
|
+
|
791
|
+
```ruby
|
792
|
+
RSpec.configure do |config|
|
793
|
+
config.include Rack::Test::Methods
|
794
|
+
end
|
795
|
+
```
|
796
|
+
|
797
|
+
Now you need to define your app method so rack-test can mount your app. Eldr apps are rack apps so we can _simply_ return a new instance of it:
|
798
|
+
|
799
|
+
```ruby
|
800
|
+
def app
|
801
|
+
YourEldrApp.new
|
802
|
+
end
|
803
|
+
```
|
804
|
+
|
805
|
+
If you want to test each of your apps individually you can use `let` to create a new Rack::Test session thingy:
|
806
|
+
|
807
|
+
```ruby
|
808
|
+
let(:rt) do
|
809
|
+
Rack::Test::Session.new YourEldrApp.new
|
810
|
+
end
|
811
|
+
```
|
812
|
+
|
813
|
+
Then you'll be able to access the rack-test methods from `rt`. For example:
|
814
|
+
|
815
|
+
```ruby
|
816
|
+
response = rt.get '/'
|
817
|
+
response.status.should == 200
|
818
|
+
```
|
819
|
+
|
820
|
+
Rack apps sure are easy to work with!
|
821
|
+
|
822
|
+
See the spec/ in this repo for some specific rspec examples.
|
823
|
+
|
824
|
+
## Performance
|
825
|
+
|
826
|
+
Eldr has been built with perfomance in mind. Right now it performs in the middle of the pack and more performance improvements are forthcoming:
|
827
|
+
|
828
|
+
| Framework | Requests/sec | % from best |
|
829
|
+
|---------------|:-------------:|------------:|
|
830
|
+
| rack | 10282.20 | 100.0% |
|
831
|
+
| hobbit | 8853.81 | 86.11% |
|
832
|
+
| roda | 8830.00 | 85.88% |
|
833
|
+
| cuba | 8517.00 | 82.83% |
|
834
|
+
| lotus-router | 8464.81 | 82.32% |
|
835
|
+
| rack-response | 7939.67 | 77.22% |
|
836
|
+
| brooklyn | 7639.88 | 74.3% |
|
837
|
+
| *eldr* | 7170.15 | 69.73% |
|
838
|
+
| rambutan | 6954.75 | 67.64% |
|
839
|
+
| nancy | 6516.84 | 63.38% |
|
840
|
+
| gin | 3850.00 | 37.44% |
|
841
|
+
| nyny | 3745.96 | 36.43% |
|
842
|
+
| sinatra | 2740.50 | 26.65% |
|
843
|
+
| rails | 2475.50 | 24.08% |
|
844
|
+
| scorched | 1692.18 | 16.46% |
|
845
|
+
| ramaze | 1490.42 | 14.5% |
|
846
|
+
|
847
|
+
See the [https://github.com/eldr-rb/bench-micro](bench-micro) repo to run your own perfomance benchmarks
|
848
|
+
|
849
|
+
## Help/Resources
|
850
|
+
|
851
|
+
You can get help from the following places:
|
852
|
+
|
853
|
+
- [@k_2052](http://twitter.com/k_2052) Feel free to mention me on twitter for quick questions
|
854
|
+
- [The Mailing List](https://groups.google.com/forum/#!forum/eldr-ruby) For questions that aren't a bug or feature request
|
855
|
+
- [The examples folder](https://github.com/eldr/eldr/tree/master/examples) in this repo has full runnable versions of the quickstart's examples
|
856
|
+
- [Issues](https://github.com/eldr/eldr/issues) on this repo for bug reports and feature requests
|
857
|
+
- [The Wiki](https://github.com/eldr/eldr/wiki) has a FAQ and links to projects using Eldrg
|
858
|
+
- And of course checkout the GitHub org [eldr/](https://github.com/eldr) for all the Eldr gems.
|
859
|
+
|
860
|
+
## Contributing
|
861
|
+
|
862
|
+
1. Fork. it
|
863
|
+
2. Create. your feature branch (git checkout -b cat-evolver)
|
864
|
+
3. Commit. your changes (git commit -am 'Add Cat Evolution')
|
865
|
+
4. Test. your changes (always be testing)
|
866
|
+
5. Push. to the branch (git push origin cat-evolver)
|
867
|
+
6. Pull. Request. (for extra points include funny gif and or pun in comments)
|
868
|
+
|
869
|
+
To remember this you can use the easy to remember and totally not tongue-in-check initialism:
|
870
|
+
FCCTPP.
|
871
|
+
|
872
|
+
I don't want any of these steps to scare you off. If you don't know how to do something or are struggle getting it to work feel free to create a pull request or issue anyway. I'll be happy to help you get your contributions up to code and into the repo!
|
873
|
+
|
874
|
+
## License
|
875
|
+
|
876
|
+
Licensed under MIT by K-2052.
|