hobby 0.0.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 +7 -0
- data/.gitignore +17 -0
- data/.travis.yml +9 -0
- data/CHANGELOG.md +53 -0
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +378 -0
- data/Rakefile +4 -0
- data/hobby.gemspec +29 -0
- data/lib/hobby/app.rb +66 -0
- data/lib/hobby/router/route.rb +20 -0
- data/lib/hobby/router.rb +29 -0
- data/lib/hobby.rb +10 -0
- data/test/helper.rb +5 -0
- data/test/test_app.rb +189 -0
- data/test/test_router.rb +65 -0
- metadata +175 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: 8c721c77a7b912fa113e730de3ff58cdf8866e06
|
4
|
+
data.tar.gz: 4590a7005e8597a277cae934955b30b0a981b1b4
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 2bb9aa2ee633d3d06c85a7e41dd1a1b59bb4d870c48de2b351846b7e38566e1f17731bb94894bcec50e917e1fbcfab51d7784d6175ae89db37476918373d3403
|
7
|
+
data.tar.gz: efa3483ccf3ea59cc6e6e16dd4280713eaef4e7a04b1ac1cd66c07e42344cd42efb6a52ddabab7cf4ed2fee032926264fb6f7224f8750e555976309d29665364
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
---
|
2
|
+
rvm:
|
3
|
+
- 2.0.0
|
4
|
+
- 2.1.2
|
5
|
+
- ruby-head
|
6
|
+
addons:
|
7
|
+
code_climate:
|
8
|
+
repo_token:
|
9
|
+
secure: "bogVqPmJ2KlI2XK3Ln9Ki8vG08WTvKbKfvYm4ErD1A+2t3vg61utFKxJS8R3P5QKuDDIIX98eQC7r+gPeGEYwT+tDDFMnBsGQZcaeXJCYZQ3qLN36GpnPmpeZ3tTZwop5aT1AmMCF0IllDWYCjcWzeEZan23Eqt5YOyzTGAnr4g="
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,53 @@
|
|
1
|
+
# 0.6.0
|
2
|
+
|
3
|
+
* Change the implementation of `Hobbit::Base#halt`. This new implementation is
|
4
|
+
more rack compliant.
|
5
|
+
* Test hobbit with [oktobertest](https://github.com/patriciomacadden/oktobertest)
|
6
|
+
instead of minitest (Because reasons!).
|
7
|
+
|
8
|
+
# 0.5.1 (Unreleased)
|
9
|
+
|
10
|
+
* A class is an object too, so allow to `run` classes.
|
11
|
+
* Add `Hobbit::Request`, which sets the path info to `/` if its empty (instead
|
12
|
+
of doing that on the call method).
|
13
|
+
|
14
|
+
# 0.5.0
|
15
|
+
|
16
|
+
* Refactor `Hobbit::Base#halt`. It now sets the status, merges the headers and
|
17
|
+
writes the body (using `Hobbit::Response#write`) when given a fixnum, a hash or
|
18
|
+
a string.
|
19
|
+
* `Hobbit::Response` headers and body are not accessors anymore. This is
|
20
|
+
because when you set the body directly, the `Content-Length` is not calculated
|
21
|
+
(it's calculated on `#write`).
|
22
|
+
|
23
|
+
# 0.4.4
|
24
|
+
|
25
|
+
* Refactor `Hobbit::Response`.
|
26
|
+
|
27
|
+
# 0.4.3
|
28
|
+
|
29
|
+
* Calculate the `Content-Length` of a `Hobbit::Response` using `#bytesize`
|
30
|
+
instead of `#size`.
|
31
|
+
|
32
|
+
# 0.4.2
|
33
|
+
|
34
|
+
* Add `Hobbit::Response#redirect`, that was missing since `Hobbit::Response`
|
35
|
+
isn't a `Rack::Response` subclass.
|
36
|
+
|
37
|
+
# 0.4.1
|
38
|
+
|
39
|
+
* `Hobbit::Response` now returns the `Content-Length` header as a string.
|
40
|
+
|
41
|
+
# 0.4.0
|
42
|
+
|
43
|
+
* Add halt method.
|
44
|
+
|
45
|
+
# 0.3.1
|
46
|
+
|
47
|
+
* Remove unused `attr_accessor` (`:length`) from `Hobbit::Response`.
|
48
|
+
|
49
|
+
# 0.3.0
|
50
|
+
|
51
|
+
* `Hobbit::Response` is no longer a subclass of `Rack::Response`.
|
52
|
+
* Forward `#map` and `#use` methods to `Rack::Builder` instead of define these
|
53
|
+
methods.
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Patricio Mac Adden
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,378 @@
|
|
1
|
+
# Hobbit [](https://travis-ci.org/patriciomacadden/hobbit) [](https://codeclimate.com/github/patriciomacadden/hobbit) [](https://codeclimate.com/github/patriciomacadden/hobbit) [](https://gemnasium.com/patriciomacadden/hobbit) [](http://badge.fury.io/rb/hobbit)
|
2
|
+
|
3
|
+
A minimalistic microframework built on top of [Rack](http://rack.github.io/).
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
```ruby
|
10
|
+
gem 'hobbit'
|
11
|
+
# or this if you want to use hobbit master
|
12
|
+
# gem 'hobbit', github: 'patriciomacadden/hobbit'
|
13
|
+
```
|
14
|
+
|
15
|
+
And then execute:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
$ bundle
|
19
|
+
```
|
20
|
+
|
21
|
+
Or install it yourself as:
|
22
|
+
|
23
|
+
```bash
|
24
|
+
$ gem install hobbit
|
25
|
+
```
|
26
|
+
|
27
|
+
## Features
|
28
|
+
|
29
|
+
* DSL inspired by [Sinatra](http://www.sinatrarb.com/).
|
30
|
+
* [Speed](https://github.com/luislavena/bench-micro).
|
31
|
+
* Extensible with standard ruby classes and modules, with no extra logic. See
|
32
|
+
[hobbit-contrib](https://github.com/patriciomacadden/hobbit-contrib).
|
33
|
+
* Zero configuration.
|
34
|
+
|
35
|
+
## Philosophy
|
36
|
+
|
37
|
+
* [Don't repeat yourself](http://en.wikipedia.org/wiki/Don't_repeat_yourself)
|
38
|
+
* Encourages the understanding and use of [Rack](http://rack.github.io/) and
|
39
|
+
its extensions instead of providing such functionality.
|
40
|
+
|
41
|
+
## Usage
|
42
|
+
|
43
|
+
Hobbit applications are just instances of classes that inherits from
|
44
|
+
`Hobbit::Base`, which complies the
|
45
|
+
[Rack SPEC](http://rubydoc.info/github/rack/rack/master/file/SPEC).
|
46
|
+
|
47
|
+
### Hello World example
|
48
|
+
|
49
|
+
Create a file called `app.rb`:
|
50
|
+
|
51
|
+
```ruby
|
52
|
+
require 'hobbit'
|
53
|
+
|
54
|
+
class App < Hobbit::Base
|
55
|
+
get '/' do
|
56
|
+
'Hello World!'
|
57
|
+
end
|
58
|
+
end
|
59
|
+
```
|
60
|
+
|
61
|
+
Create a `config.ru` file:
|
62
|
+
|
63
|
+
```ruby
|
64
|
+
require './app'
|
65
|
+
|
66
|
+
run App.new # or just `run App`
|
67
|
+
```
|
68
|
+
|
69
|
+
Run it with `rackup`:
|
70
|
+
|
71
|
+
```bash
|
72
|
+
$ rackup
|
73
|
+
```
|
74
|
+
|
75
|
+
View your app at [http://localhost:9292](http://localhost:9292).
|
76
|
+
|
77
|
+
### Routes
|
78
|
+
|
79
|
+
Every route is composed of a verb, a path (optional) and a block. When an
|
80
|
+
incoming request matches a route, the block is executed and a response is sent
|
81
|
+
back to the client. The return value of the block will be the `body` of the
|
82
|
+
response. The `headers` and `status code` of the response will be calculated by
|
83
|
+
`Hobbit::Response`, but you could modify it anyway you want it.
|
84
|
+
|
85
|
+
See an example:
|
86
|
+
|
87
|
+
```ruby
|
88
|
+
class App < Hobbit::Base
|
89
|
+
get '/' do
|
90
|
+
# ...
|
91
|
+
end
|
92
|
+
|
93
|
+
post '/' do
|
94
|
+
# ...
|
95
|
+
end
|
96
|
+
|
97
|
+
put '/' do
|
98
|
+
# ...
|
99
|
+
end
|
100
|
+
|
101
|
+
patch '/' do
|
102
|
+
# ...
|
103
|
+
end
|
104
|
+
|
105
|
+
delete '/' do
|
106
|
+
# ...
|
107
|
+
end
|
108
|
+
|
109
|
+
options '/' do
|
110
|
+
# ...
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
115
|
+
When a route gets called you have this methods available:
|
116
|
+
|
117
|
+
* `env`: The Rack environment.
|
118
|
+
* `request`: a `Hobbit::Request` instance.
|
119
|
+
* `response`: a `Hobbit::Response` instance.
|
120
|
+
|
121
|
+
And any other method defined in your application.
|
122
|
+
|
123
|
+
#### Available methods
|
124
|
+
|
125
|
+
* `delete`
|
126
|
+
* `get`
|
127
|
+
* `head`
|
128
|
+
* `options`
|
129
|
+
* `patch`
|
130
|
+
* `post`
|
131
|
+
* `put`
|
132
|
+
|
133
|
+
**Note**: Since most browsers don't support methods other than **GET** and
|
134
|
+
**POST** you must use the `Rack::MethodOverride` middleware. (See
|
135
|
+
[Rack::MethodOverride](https://github.com/rack/rack/blob/master/lib/rack/methodoverride.rb)).
|
136
|
+
|
137
|
+
#### Routes with parameters
|
138
|
+
|
139
|
+
Besides the standard `GET` and `POST` parameters, you can have routes with
|
140
|
+
parameters:
|
141
|
+
|
142
|
+
```ruby
|
143
|
+
require 'hobbit'
|
144
|
+
|
145
|
+
class App < Hobbit::Base
|
146
|
+
# matches both /hi/hobbit and /hi/patricio
|
147
|
+
get '/hi/:name' do
|
148
|
+
# request.params is filled with the route paramters, like this:
|
149
|
+
"Hello #{request.params[:name]}"
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
154
|
+
#### Redirecting
|
155
|
+
|
156
|
+
If you look at Hobbit implementation, you may notice that there is no
|
157
|
+
`redirect` method (or similar). This is because such functionality is provided
|
158
|
+
by [Rack::Response](https://github.com/rack/rack/blob/master/lib/rack/response.rb)
|
159
|
+
and for now we [don't wan't to repeat ourselves](http://en.wikipedia.org/wiki/Don't_repeat_yourself)
|
160
|
+
(obviously you can create an extension!). So, if you want to redirect to
|
161
|
+
another route, do it like this:
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
require 'hobbit'
|
165
|
+
|
166
|
+
class App < Hobbit::Base
|
167
|
+
get '/' do
|
168
|
+
response.redirect '/hi'
|
169
|
+
end
|
170
|
+
|
171
|
+
get '/hi' do
|
172
|
+
'Hello World!'
|
173
|
+
end
|
174
|
+
end
|
175
|
+
```
|
176
|
+
|
177
|
+
#### Halting
|
178
|
+
|
179
|
+
To immediately stop a request within route you can use `halt`.
|
180
|
+
|
181
|
+
```ruby
|
182
|
+
require 'hobbit'
|
183
|
+
|
184
|
+
class App < Hobbit::Base
|
185
|
+
use Rack::Session::Cookie, secret: SecureRandom.hex(64)
|
186
|
+
|
187
|
+
def session
|
188
|
+
env['rack.session']
|
189
|
+
end
|
190
|
+
|
191
|
+
get '/' do
|
192
|
+
response.status = 401
|
193
|
+
halt response.finish
|
194
|
+
end
|
195
|
+
end
|
196
|
+
```
|
197
|
+
|
198
|
+
### Built on top of rack
|
199
|
+
|
200
|
+
Each Hobbit application is a Rack stack (See this
|
201
|
+
[blog post](http://m.onkey.org/ruby-on-rack-2-the-builder) for more
|
202
|
+
information).
|
203
|
+
|
204
|
+
#### Mapping applications
|
205
|
+
|
206
|
+
You can mount any Rack application to the stack by using the `map` class
|
207
|
+
method:
|
208
|
+
|
209
|
+
```ruby
|
210
|
+
require 'hobbit'
|
211
|
+
|
212
|
+
class InnerApp < Hobbit::Base
|
213
|
+
# gets called when path_info = '/inner'
|
214
|
+
get do
|
215
|
+
'Hello InnerApp!'
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
class App < Hobbit::Base
|
220
|
+
map('/inner') { run InnerApp.new }
|
221
|
+
|
222
|
+
get '/' do
|
223
|
+
'Hello App!'
|
224
|
+
end
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
228
|
+
#### Using middleware
|
229
|
+
|
230
|
+
You can add any Rack middleware to the stack by using the `use` class method:
|
231
|
+
|
232
|
+
```ruby
|
233
|
+
require 'hobbit'
|
234
|
+
|
235
|
+
class App < Hobbit::Base
|
236
|
+
use Rack::Session::Cookie, secret: SecureRandom.hex(64)
|
237
|
+
use Rack::ShowExceptions
|
238
|
+
|
239
|
+
def session
|
240
|
+
env['rack.session']
|
241
|
+
end
|
242
|
+
|
243
|
+
get '/' do
|
244
|
+
session[:name] = 'hobbit'
|
245
|
+
end
|
246
|
+
|
247
|
+
# more routes...
|
248
|
+
end
|
249
|
+
|
250
|
+
run App.new
|
251
|
+
```
|
252
|
+
|
253
|
+
### Security
|
254
|
+
|
255
|
+
By default, Hobbit (nor Rack) comes without any protection against web
|
256
|
+
attacks. The use of [rack-protection](https://github.com/rkh/rack-protection)
|
257
|
+
is highly recommended:
|
258
|
+
|
259
|
+
```ruby
|
260
|
+
require 'hobbit'
|
261
|
+
require 'rack/protection'
|
262
|
+
require 'securerandom'
|
263
|
+
|
264
|
+
class App < Hobbit::Base
|
265
|
+
use Rack::Session::Cookie, secret: SecureRandom.hex(64)
|
266
|
+
use Rack::Protection
|
267
|
+
|
268
|
+
get '/' do
|
269
|
+
'Hello World!'
|
270
|
+
end
|
271
|
+
end
|
272
|
+
```
|
273
|
+
|
274
|
+
See the [rack-protection](https://github.com/rkh/rack-protection)
|
275
|
+
documentation for futher information.
|
276
|
+
|
277
|
+
### Testing
|
278
|
+
|
279
|
+
[rack-test](https://github.com/brynary/rack-test) is highly recommended. See
|
280
|
+
an example:
|
281
|
+
|
282
|
+
In `app.rb`:
|
283
|
+
|
284
|
+
```ruby
|
285
|
+
require 'hobbit'
|
286
|
+
|
287
|
+
class App < Hobbit::Base
|
288
|
+
get '/' do
|
289
|
+
'Hello World!'
|
290
|
+
end
|
291
|
+
end
|
292
|
+
```
|
293
|
+
|
294
|
+
In `app_spec.rb`:
|
295
|
+
|
296
|
+
```ruby
|
297
|
+
require 'minitest/autorun'
|
298
|
+
# imagine that app.rb and app_spec.rb are stored in the same directory
|
299
|
+
require 'app'
|
300
|
+
|
301
|
+
describe App do
|
302
|
+
include Rack::Test::Methods
|
303
|
+
|
304
|
+
def app
|
305
|
+
App.new
|
306
|
+
end
|
307
|
+
|
308
|
+
describe 'GET /' do
|
309
|
+
it 'must be ok' do
|
310
|
+
get '/'
|
311
|
+
last_response.must_be :ok?
|
312
|
+
last_response.body.must_match /Hello World!/
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
```
|
317
|
+
|
318
|
+
See the [rack-test](https://github.com/brynary/rack-test) documentation
|
319
|
+
for futher information.
|
320
|
+
|
321
|
+
### Extensions
|
322
|
+
|
323
|
+
You can extend Hobbit by creating standard ruby modules. See an example:
|
324
|
+
|
325
|
+
```ruby
|
326
|
+
module MyExtension
|
327
|
+
def do_something
|
328
|
+
# do something
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
332
|
+
class App < Hobbit::Base
|
333
|
+
include MyExtension
|
334
|
+
|
335
|
+
get '/' do
|
336
|
+
do_something
|
337
|
+
'Hello World!'
|
338
|
+
end
|
339
|
+
end
|
340
|
+
```
|
341
|
+
|
342
|
+
#### Hobbit::Contrib
|
343
|
+
|
344
|
+
[hobbit-contrib](https://github.com/patriciomacadden/hobbit-contrib) is a ruby
|
345
|
+
gem that comes with a lot of hobbit extensions, such as:
|
346
|
+
|
347
|
+
* `Hobbit::Render`: provides basic template rendering.
|
348
|
+
* `Hobbit::Session`: provides helper methods for handling user sessions.
|
349
|
+
* `Hobbit::Environment`: provides helper methods for handling application
|
350
|
+
environments.
|
351
|
+
* `Hobbit::Filter`: provides helper class methods for handling Sinatra-like
|
352
|
+
filters.
|
353
|
+
* `Hobbit::ErrorHandling`: provides helper class methods for handling
|
354
|
+
Sinatra-like error handling.
|
355
|
+
|
356
|
+
... And many more!
|
357
|
+
|
358
|
+
## Community
|
359
|
+
|
360
|
+
* [Wiki](https://github.com/patriciomacadden/hobbit/wiki): Guides, how-tos and recipes
|
361
|
+
* IRC: [#hobbitrb](irc://chat.freenode.net/#hobbitrb) on [http://freenode.net](http://freenode.net)
|
362
|
+
|
363
|
+
## Presentations
|
364
|
+
|
365
|
+
* Building web applications in Ruby, by [Krzysztof Wawer](https://github.com/wafcio)
|
366
|
+
([english](https://speakerdeck.com/wafcio/hobbit-english), [polish](https://speakerdeck.com/wafcio/hobbit))
|
367
|
+
|
368
|
+
## Contributing
|
369
|
+
|
370
|
+
1. Fork it
|
371
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
372
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
373
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
374
|
+
5. Create new Pull Request
|
375
|
+
|
376
|
+
## License
|
377
|
+
|
378
|
+
See the [LICENSE](https://github.com/patriciomacadden/hobbit/blob/master/LICENSE).
|
data/Rakefile
ADDED
data/hobby.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = 'hobby'
|
7
|
+
spec.version = '0.0.0'
|
8
|
+
spec.authors = ['Patricio Mac Adden']
|
9
|
+
spec.email = ['patriciomacadden@gmail.com']
|
10
|
+
spec.description = %q{A minimalistic microframework built on top of rack}
|
11
|
+
spec.summary = %q{A minimalistic microframework built on top of rack}
|
12
|
+
spec.homepage = 'https://github.com/patriciomacadden/hobbit'
|
13
|
+
spec.license = 'MIT'
|
14
|
+
|
15
|
+
spec.files = `git ls-files`.split($/)
|
16
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
17
|
+
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
|
+
spec.require_paths = ['lib']
|
19
|
+
|
20
|
+
spec.add_dependency 'rack'
|
21
|
+
spec.add_dependency 'include_constants'
|
22
|
+
|
23
|
+
spec.add_development_dependency 'bundler', '~> 1.3'
|
24
|
+
spec.add_development_dependency 'codeclimate-test-reporter'
|
25
|
+
spec.add_development_dependency 'rack-test'
|
26
|
+
spec.add_development_dependency 'rake'
|
27
|
+
spec.add_development_dependency 'minitest'
|
28
|
+
spec.add_development_dependency 'minitest-power_assert'
|
29
|
+
end
|
data/lib/hobby/app.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
module Hobby
|
2
|
+
class App
|
3
|
+
class << self
|
4
|
+
def members
|
5
|
+
@members ||= {}
|
6
|
+
end
|
7
|
+
|
8
|
+
[:builder, :router].each do |member|
|
9
|
+
define_method member do |&custom_member|
|
10
|
+
if custom_member
|
11
|
+
members[member] = custom_member.call
|
12
|
+
else
|
13
|
+
members[member] ||= Hobby.const_get(member.capitalize).new
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
Verbs.each do |verb|
|
19
|
+
define_method verb.downcase do |path, &route|
|
20
|
+
router.add_route verb, path, &route
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
alias :_new :new
|
25
|
+
def new *args, &block
|
26
|
+
builder.run _new(*args, &block)
|
27
|
+
builder
|
28
|
+
end
|
29
|
+
|
30
|
+
extend Forwardable
|
31
|
+
delegate [:map, :use] => :builder
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :env, :request, :response
|
35
|
+
|
36
|
+
def call env
|
37
|
+
dup.handle env
|
38
|
+
end
|
39
|
+
|
40
|
+
def handle env
|
41
|
+
@env = env
|
42
|
+
@request = Request.new @env
|
43
|
+
@response = Response.new
|
44
|
+
|
45
|
+
catch(:halt) { route_eval }
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def halt response
|
51
|
+
throw :halt, response
|
52
|
+
end
|
53
|
+
|
54
|
+
def route_eval
|
55
|
+
route = self.class.router.route_for request
|
56
|
+
|
57
|
+
if route
|
58
|
+
response.write instance_eval &route
|
59
|
+
else
|
60
|
+
response.status = 404
|
61
|
+
end
|
62
|
+
|
63
|
+
response.finish
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Hobby::Router
|
2
|
+
class Route
|
3
|
+
attr_reader :compiled_path, :extra_params, :path
|
4
|
+
def initialize(path, &block)
|
5
|
+
@path = path
|
6
|
+
@block = block
|
7
|
+
|
8
|
+
@extra_params = []
|
9
|
+
compiled_path = path.gsub(/:\w+/) do |match|
|
10
|
+
@extra_params << match.gsub(':', '').to_sym
|
11
|
+
'([^/?#]+)'
|
12
|
+
end
|
13
|
+
@compiled_path = /^#{compiled_path}$/
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_proc
|
17
|
+
@block
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
data/lib/hobby/router.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
module Hobby
|
2
|
+
class Router
|
3
|
+
require_relative 'router/route'
|
4
|
+
|
5
|
+
def initialize
|
6
|
+
@routes = Hash.new { |hash, key| hash[key] = [] }
|
7
|
+
end
|
8
|
+
|
9
|
+
def add_route(verb, path, &block)
|
10
|
+
@routes[verb] << Route.new(path, &block)
|
11
|
+
self
|
12
|
+
end
|
13
|
+
|
14
|
+
def route_for(request)
|
15
|
+
route = @routes[request.request_method].detect do |route|
|
16
|
+
route.compiled_path =~ request.path_info
|
17
|
+
end
|
18
|
+
|
19
|
+
if route
|
20
|
+
$~.captures.each_with_index do |value, index|
|
21
|
+
param = route.extra_params[index]
|
22
|
+
request.params[param] = value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
route
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/hobby.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require 'rack'
|
2
|
+
require 'forwardable'
|
3
|
+
require 'include_constants'
|
4
|
+
|
5
|
+
module Hobby
|
6
|
+
Verbs = %w!DELETE GET HEAD OPTIONS PATCH POST PUT!
|
7
|
+
require 'hobby/app'
|
8
|
+
autoload :Router, 'hobby/router'
|
9
|
+
include_constants :Builder, :Request, :Response, from: Rack
|
10
|
+
end
|
data/test/helper.rb
ADDED
data/test/test_app.rb
ADDED
@@ -0,0 +1,189 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
module Minitest
|
4
|
+
class Test
|
5
|
+
include Rack::Test::Methods
|
6
|
+
|
7
|
+
def mock_app(&block)
|
8
|
+
@app = Class.new(Hobby::App, &block).new
|
9
|
+
end
|
10
|
+
|
11
|
+
def app
|
12
|
+
@app
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe Hobby::App do
|
18
|
+
describe :main_app do
|
19
|
+
before do
|
20
|
+
mock_app do
|
21
|
+
Hobby::Verbs.each do |verb|
|
22
|
+
class_eval "#{verb.downcase}('/') { '#{verb}' }"
|
23
|
+
class_eval "#{verb.downcase}('/route.json') { '#{verb} /route.json' }"
|
24
|
+
class_eval "#{verb.downcase}('/route/:id.json') { request.params[:id] }"
|
25
|
+
class_eval "#{verb.downcase}('/:name') { request.params[:name] }"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
Hobby::Verbs.each do |verb|
|
31
|
+
describe 'when the request matches a route' do
|
32
|
+
it "matches #{verb} ''" do
|
33
|
+
send verb.downcase, ''
|
34
|
+
assert last_response.ok?
|
35
|
+
assert_equal verb, last_response.body
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'matches #{verb} /' do
|
39
|
+
send verb.downcase, '/'
|
40
|
+
assert last_response.ok?
|
41
|
+
assert_equal verb, last_response.body
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'matches #{verb} /route.json' do
|
45
|
+
send verb.downcase, '/route.json'
|
46
|
+
assert last_response.ok?
|
47
|
+
assert_equal "#{verb} /route.json", last_response.body
|
48
|
+
end
|
49
|
+
|
50
|
+
it 'matches #{verb} /route/:id.json' do
|
51
|
+
send verb.downcase, '/route/1.json'
|
52
|
+
assert last_response.ok?
|
53
|
+
assert_equal '1', last_response.body
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'matches #{verb} /:name' do
|
57
|
+
send verb.downcase, '/hobbit'
|
58
|
+
assert last_response.ok?
|
59
|
+
assert_equal 'hobbit', last_response.body
|
60
|
+
|
61
|
+
send verb.downcase, '/hello-hobbit'
|
62
|
+
assert last_response.ok?
|
63
|
+
assert_equal 'hello-hobbit', last_response.body
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
describe 'when the request not matches a route' do
|
68
|
+
it 'responds with 404 status code' do
|
69
|
+
send verb.downcase, '/not/found'
|
70
|
+
assert last_response.not_found?
|
71
|
+
assert_equal '', last_response.body
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
describe :map_app do
|
78
|
+
before do
|
79
|
+
mock_app do
|
80
|
+
map '/map' do
|
81
|
+
run Proc.new { |env| [200, {}, ['from map']] }
|
82
|
+
end
|
83
|
+
|
84
|
+
get('/') { 'hello world' }
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'mounts an application to the rack stack' do
|
89
|
+
get '/map'
|
90
|
+
assert_equal 'from map', last_response.body
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
describe :use_app do
|
95
|
+
before do
|
96
|
+
mock_app do
|
97
|
+
middleware = Class.new do
|
98
|
+
def initialize(app = nil)
|
99
|
+
@app = app
|
100
|
+
end
|
101
|
+
|
102
|
+
def call(env)
|
103
|
+
request = Rack::Request.new(env)
|
104
|
+
@app.call(env) unless request.path_info == '/use'
|
105
|
+
[200, {}, 'from use']
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
use middleware
|
110
|
+
|
111
|
+
get('/') { 'hello world' }
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'adds a middleware to the rack stack' do
|
116
|
+
get '/use'
|
117
|
+
assert_equal 'from use', last_response.body
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe :halt_app do
|
122
|
+
before do
|
123
|
+
mock_app do
|
124
|
+
get '/halt' do
|
125
|
+
response.status = 501
|
126
|
+
halt response.finish
|
127
|
+
end
|
128
|
+
|
129
|
+
get '/halt_finished' do
|
130
|
+
halt [404, {}, ['Not found']]
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
it 'halts the execution with a response' do
|
136
|
+
get '/halt'
|
137
|
+
assert { last_response.status == 501 }
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'halts the execution with a finished response' do
|
141
|
+
get '/halt_finished'
|
142
|
+
assert { last_response.status == 404 }
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe :router_app do
|
147
|
+
before do
|
148
|
+
mock_app do
|
149
|
+
router do
|
150
|
+
Class.new do
|
151
|
+
def add_route(*)
|
152
|
+
end
|
153
|
+
|
154
|
+
def route_for _request
|
155
|
+
Proc.new { 'for any route' }
|
156
|
+
end
|
157
|
+
end.new
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
it 'returns for any route' do
|
163
|
+
get '/'
|
164
|
+
assert { last_response.body == 'for any route' }
|
165
|
+
|
166
|
+
get '/some-other-route'
|
167
|
+
assert { last_response.body == 'for any route' }
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
describe :custom_members do
|
172
|
+
before do
|
173
|
+
mock_app do
|
174
|
+
builder { Rack::Builder.new }
|
175
|
+
Request = Rack::Request
|
176
|
+
Response = Rack::Response
|
177
|
+
|
178
|
+
get '/' do
|
179
|
+
'it works'
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
it 'works' do
|
185
|
+
get '/'
|
186
|
+
assert { last_response.body == 'it works' }
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
data/test/test_router.rb
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
require_relative 'helper'
|
2
|
+
|
3
|
+
def request_to path, request_method: 'GET'
|
4
|
+
Hobby::Request.new Rack::MockRequest.env_for "http://example.com:8080/#{path}", method: request_method
|
5
|
+
end
|
6
|
+
|
7
|
+
describe Hobby::Router do
|
8
|
+
before do
|
9
|
+
@router = Hobby::Router.new
|
10
|
+
@route = -> { :wrapped }
|
11
|
+
end
|
12
|
+
|
13
|
+
it 'works' do
|
14
|
+
@router.add_route 'GET', '/ololo', &@route
|
15
|
+
|
16
|
+
route = @router.route_for request_to 'ololo'
|
17
|
+
assert { route.to_proc.call == :wrapped }
|
18
|
+
|
19
|
+
route = @router.route_for request_to 'ololo2'
|
20
|
+
assert { route.nil? }
|
21
|
+
|
22
|
+
route = @router.route_for request_to 'ololo', request_method: 'POST'
|
23
|
+
assert { route.nil? }
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'with .' do
|
27
|
+
@router.add_route 'GET', '/route.json', &@route
|
28
|
+
|
29
|
+
route = @router.route_for request_to 'route.json'
|
30
|
+
assert { route.to_proc.call == :wrapped }
|
31
|
+
end
|
32
|
+
|
33
|
+
it 'with -' do
|
34
|
+
@router.add_route 'GET', '/hello-world', &@route
|
35
|
+
|
36
|
+
route = @router.route_for request_to 'hello-world'
|
37
|
+
assert { route.to_proc.call == :wrapped }
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'with params' do
|
41
|
+
@router
|
42
|
+
.add_route 'GET', '/hello/:name' do :first end
|
43
|
+
.add_route 'GET', '/say/:something/to/:someone' do :second end
|
44
|
+
|
45
|
+
request = request_to 'hello/ololo'
|
46
|
+
route = @router.route_for request
|
47
|
+
assert { route.to_proc.call == :first }
|
48
|
+
assert { request.params[:name] == 'ololo'}
|
49
|
+
|
50
|
+
request = request_to 'say/nothing/to/no_one'
|
51
|
+
route = @router.route_for request
|
52
|
+
assert { route.to_proc.call == :second }
|
53
|
+
assert { request.params[:something] == 'nothing'}
|
54
|
+
assert { request.params[:someone] == 'no_one'}
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'with . and params' do
|
58
|
+
@router.add_route 'GET', '/route/:id.json', &@route
|
59
|
+
|
60
|
+
request = request_to 'route/42.json'
|
61
|
+
route = @router.route_for request
|
62
|
+
assert { route.to_proc.call == :wrapped }
|
63
|
+
assert { request.params[:id] == '42' }
|
64
|
+
end
|
65
|
+
end
|
metadata
ADDED
@@ -0,0 +1,175 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: hobby
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Patricio Mac Adden
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2015-08-16 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: rack
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: include_constants
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.3'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.3'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: codeclimate-test-reporter
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rack-test
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rake
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - ">="
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: minitest
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - ">="
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - ">="
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest-power_assert
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
description: A minimalistic microframework built on top of rack
|
126
|
+
email:
|
127
|
+
- patriciomacadden@gmail.com
|
128
|
+
executables: []
|
129
|
+
extensions: []
|
130
|
+
extra_rdoc_files: []
|
131
|
+
files:
|
132
|
+
- ".gitignore"
|
133
|
+
- ".travis.yml"
|
134
|
+
- CHANGELOG.md
|
135
|
+
- Gemfile
|
136
|
+
- LICENSE
|
137
|
+
- README.md
|
138
|
+
- Rakefile
|
139
|
+
- hobby.gemspec
|
140
|
+
- lib/hobby.rb
|
141
|
+
- lib/hobby/app.rb
|
142
|
+
- lib/hobby/router.rb
|
143
|
+
- lib/hobby/router/route.rb
|
144
|
+
- test/helper.rb
|
145
|
+
- test/test_app.rb
|
146
|
+
- test/test_router.rb
|
147
|
+
homepage: https://github.com/patriciomacadden/hobbit
|
148
|
+
licenses:
|
149
|
+
- MIT
|
150
|
+
metadata: {}
|
151
|
+
post_install_message:
|
152
|
+
rdoc_options: []
|
153
|
+
require_paths:
|
154
|
+
- lib
|
155
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
156
|
+
requirements:
|
157
|
+
- - ">="
|
158
|
+
- !ruby/object:Gem::Version
|
159
|
+
version: '0'
|
160
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
161
|
+
requirements:
|
162
|
+
- - ">="
|
163
|
+
- !ruby/object:Gem::Version
|
164
|
+
version: '0'
|
165
|
+
requirements: []
|
166
|
+
rubyforge_project:
|
167
|
+
rubygems_version: 2.4.5
|
168
|
+
signing_key:
|
169
|
+
specification_version: 4
|
170
|
+
summary: A minimalistic microframework built on top of rack
|
171
|
+
test_files:
|
172
|
+
- test/helper.rb
|
173
|
+
- test/test_app.rb
|
174
|
+
- test/test_router.rb
|
175
|
+
has_rdoc:
|