lotus-router 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 +4 -4
- data/.coveralls.yml +2 -0
- data/.gitignore +5 -13
- data/.travis.yml +5 -0
- data/.yardopts +3 -0
- data/Gemfile +10 -3
- data/README.md +470 -6
- data/Rakefile +10 -1
- data/benchmarks/callable +23 -0
- data/benchmarks/named_routes +72 -0
- data/benchmarks/resource +44 -0
- data/benchmarks/resources +58 -0
- data/benchmarks/routes +67 -0
- data/benchmarks/run.sh +11 -0
- data/benchmarks/utils.rb +56 -0
- data/lib/lotus-router.rb +1 -0
- data/lib/lotus/router.rb +752 -3
- data/lib/lotus/router/version.rb +2 -2
- data/lib/lotus/routing/endpoint.rb +114 -0
- data/lib/lotus/routing/endpoint_resolver.rb +251 -0
- data/lib/lotus/routing/http_router.rb +130 -0
- data/lib/lotus/routing/namespace.rb +86 -0
- data/lib/lotus/routing/resource.rb +73 -0
- data/lib/lotus/routing/resource/action.rb +340 -0
- data/lib/lotus/routing/resource/options.rb +48 -0
- data/lib/lotus/routing/resources.rb +40 -0
- data/lib/lotus/routing/resources/action.rb +123 -0
- data/lib/lotus/routing/route.rb +53 -0
- data/lotus-router.gemspec +16 -12
- data/test/fixtures.rb +193 -0
- data/test/integration/client_error_test.rb +16 -0
- data/test/integration/pass_on_response_test.rb +13 -0
- data/test/named_routes_test.rb +123 -0
- data/test/namespace_test.rb +289 -0
- data/test/new_test.rb +67 -0
- data/test/redirect_test.rb +33 -0
- data/test/resource_test.rb +128 -0
- data/test/resources_test.rb +136 -0
- data/test/routing/endpoint_resolver_test.rb +110 -0
- data/test/routing/resource/options_test.rb +36 -0
- data/test/routing_test.rb +99 -0
- data/test/test_helper.rb +32 -0
- data/test/version_test.rb +7 -0
- metadata +102 -10
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ff45eb9a0d8db10815f562e9d5af4e5cf2649b7a
|
4
|
+
data.tar.gz: 3a6e353ac3840fc2bab5a0324ec50294493efa20
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5aba74771a3578cb3654a687b428e9fdca6bbdc2cc186dc1777ff28b9862a4a9f50d7daa9de80fbfc191bcf67ca3ab35eabfdcdbffedfb9d0043aa62601d8b72
|
7
|
+
data.tar.gz: e0d18ad2c7e1cdb275a58871a321cd635b383c1c6308635cfeab29bfe5859e62eeeab0c728414ba118601787855a3b3e01fc602120ddf0c45304af63b20d84e4
|
data/.coveralls.yml
ADDED
data/.gitignore
CHANGED
@@ -1,17 +1,9 @@
|
|
1
1
|
*.gem
|
2
|
-
|
2
|
+
.devnotes
|
3
|
+
.greenbar
|
3
4
|
.bundle
|
4
|
-
.config
|
5
|
-
.yardoc
|
6
5
|
Gemfile.lock
|
7
|
-
|
8
|
-
_yardoc
|
9
|
-
coverage
|
6
|
+
.yardoc
|
10
7
|
doc/
|
11
|
-
|
12
|
-
|
13
|
-
rdoc
|
14
|
-
spec/reports
|
15
|
-
test/tmp
|
16
|
-
test/version_tmp
|
17
|
-
tmp
|
8
|
+
coverage/
|
9
|
+
tmp/
|
data/.travis.yml
ADDED
data/.yardopts
ADDED
data/Gemfile
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
-
source '
|
2
|
-
|
3
|
-
# Specify your gem's dependencies in lotus-router.gemspec
|
1
|
+
source 'http://rubygems.org'
|
4
2
|
gemspec
|
3
|
+
|
4
|
+
unless ENV['TRAVIS']
|
5
|
+
gem 'debugger', require: false, platforms: :ruby
|
6
|
+
gem 'yard', require: false
|
7
|
+
gem 'simplecov', require: false
|
8
|
+
gem 'lotus-utils', require: false, path: '../lotus-utils'
|
9
|
+
end
|
10
|
+
|
11
|
+
gem 'coveralls', require: false
|
data/README.md
CHANGED
@@ -1,29 +1,493 @@
|
|
1
1
|
# Lotus::Router
|
2
2
|
|
3
|
-
|
3
|
+
Rack compatible, lightweight and fast HTTP Router for [Lotus](http://lotusrb.org).
|
4
|
+
|
5
|
+
## Status
|
6
|
+
|
7
|
+
[](http://badge.fury.io/rb/lotus-router)
|
8
|
+
[](http://travis-ci.org/lotus/router?branch=master)
|
9
|
+
[](https://coveralls.io/r/lotus/router)
|
10
|
+
[](https://codeclimate.com/github/lotus/router)
|
11
|
+
[](https://gemnasium.com/lotus/router)
|
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-router
|
18
|
+
* Bugs/Issues: https://github.com/lotus/router/issues
|
19
|
+
* Support: http://stackoverflow.com/questions/tagged/lotusrb
|
20
|
+
|
21
|
+
## Rubies
|
22
|
+
|
23
|
+
__Lotus::Router__ supports Ruby (MRI) 2+
|
24
|
+
|
4
25
|
|
5
26
|
## Installation
|
6
27
|
|
7
28
|
Add this line to your application's Gemfile:
|
8
29
|
|
9
|
-
|
30
|
+
```ruby
|
31
|
+
gem 'lotus-router'
|
32
|
+
```
|
10
33
|
|
11
34
|
And then execute:
|
12
35
|
|
13
|
-
|
36
|
+
```shell
|
37
|
+
$ bundle
|
38
|
+
```
|
14
39
|
|
15
40
|
Or install it yourself as:
|
16
41
|
|
17
|
-
|
42
|
+
```shell
|
43
|
+
$ gem install lotus-router
|
44
|
+
```
|
45
|
+
|
46
|
+
## Getting Started
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
require 'lotus/router'
|
50
|
+
|
51
|
+
app = Lotus::Router.new do
|
52
|
+
get '/', to: ->(env) { [200, {}, ['Welcome to Lotus::Router!']] }
|
53
|
+
end
|
54
|
+
|
55
|
+
Rack::Server.start app: app, Port: 2306
|
56
|
+
```
|
18
57
|
|
19
58
|
## Usage
|
20
59
|
|
21
|
-
|
60
|
+
__Lotus::Router__ is designed to work as a standalone framework or within a
|
61
|
+
context of a [Lotus](http://lotusrb.org) application.
|
62
|
+
|
63
|
+
For the standalone usage, it supports neat features:
|
64
|
+
|
65
|
+
### A Beautiful DSL:
|
66
|
+
|
67
|
+
```ruby
|
68
|
+
Lotus::Router.new do
|
69
|
+
get '/', to: ->(env) { [200, {}, ['Hi!']] }
|
70
|
+
get '/dashboard', to: DashboardController::Index
|
71
|
+
get '/rack-app', to: RackApp.new
|
72
|
+
get '/flowers', to: 'flowers#index'
|
73
|
+
get '/flowers/:id', to: 'flowers#show'
|
74
|
+
|
75
|
+
redirect '/legacy', to: '/'
|
76
|
+
|
77
|
+
namespace 'admin' do
|
78
|
+
get '/users', to: UsersController::Index
|
79
|
+
end
|
80
|
+
|
81
|
+
resource 'identity' do
|
82
|
+
member do
|
83
|
+
get '/avatar'
|
84
|
+
end
|
85
|
+
|
86
|
+
collection do
|
87
|
+
get '/api_keys'
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
resources 'robots' do
|
92
|
+
member do
|
93
|
+
patch '/activate'
|
94
|
+
end
|
95
|
+
|
96
|
+
collection do
|
97
|
+
get '/search'
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
```
|
102
|
+
|
103
|
+
|
104
|
+
|
105
|
+
### Fixed string matching:
|
106
|
+
|
107
|
+
```ruby
|
108
|
+
router = Lotus::Router.new
|
109
|
+
router.get '/lotus', to: ->(env) { [200, {}, ['Hello from Lotus!']] }
|
110
|
+
```
|
111
|
+
|
112
|
+
|
113
|
+
|
114
|
+
### String matching with variables:
|
115
|
+
|
116
|
+
```ruby
|
117
|
+
router = Lotus::Router.new
|
118
|
+
router.get '/flowers/:id', to: ->(env) { [200, {}, ["Hello from Flower no. #{ env['router.params'][:id] }!"]] }
|
119
|
+
```
|
120
|
+
|
121
|
+
|
122
|
+
|
123
|
+
### Variables Constraints:
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
router = Lotus::Router.new
|
127
|
+
router.get '/flowers/:id', id: /\d+/, to: ->(env) { [200, {}, [":id must be a number!"]] }
|
128
|
+
```
|
129
|
+
|
130
|
+
|
131
|
+
|
132
|
+
### String matching with globbling:
|
133
|
+
|
134
|
+
```ruby
|
135
|
+
router = Lotus::Router.new
|
136
|
+
router.get '/*', to: ->(env) { [200, {}, ["This is catch all: #{ env['router.params'].inspect }!"]] }
|
137
|
+
```
|
138
|
+
|
139
|
+
|
140
|
+
|
141
|
+
### String matching with optional tokens:
|
142
|
+
|
143
|
+
```ruby
|
144
|
+
router = Lotus::Router.new
|
145
|
+
router.get '/lotus(.:format)' to: ->(env) { [200, {}, ["You've requested #{ env['router.params'][:format] }!"]] }
|
146
|
+
```
|
147
|
+
|
148
|
+
|
149
|
+
|
150
|
+
### Support for the most common HTTP methods:
|
151
|
+
|
152
|
+
```ruby
|
153
|
+
router = Lotus::Router.new
|
154
|
+
endpoint = ->(env) { [200, {}, ['Hello from Lotus!']] }
|
155
|
+
|
156
|
+
router.get '/lotus', to: endpoint
|
157
|
+
router.post '/lotus', to: endpoint
|
158
|
+
router.put '/lotus', to: endpoint
|
159
|
+
router.patch '/lotus', to: endpoint
|
160
|
+
router.delete '/lotus', to: endpoint
|
161
|
+
router.trace '/lotus', to: endpoint
|
162
|
+
```
|
163
|
+
|
164
|
+
|
165
|
+
|
166
|
+
### Redirect:
|
167
|
+
|
168
|
+
```ruby
|
169
|
+
router = Lotus::Router.new
|
170
|
+
router.get '/redirect_destination', to: ->(env) { [200, {}, ['Redirect destination!']] }
|
171
|
+
router.redirect '/legacy', to: '/redirect_destination'
|
172
|
+
```
|
173
|
+
|
174
|
+
|
175
|
+
|
176
|
+
### Named routes:
|
177
|
+
|
178
|
+
```ruby
|
179
|
+
router = Lotus::Router.new(scheme: 'https', host: 'lotusrb.org')
|
180
|
+
router.get '/lotus', to: ->(env) { [200, {}, ['Hello from Lotus!']] }, as: :lotus
|
181
|
+
|
182
|
+
router.path(:lotus) # => "/lotus"
|
183
|
+
router.url(:lotus) # => "https://lotusrb.org/lotus"
|
184
|
+
```
|
185
|
+
|
186
|
+
|
187
|
+
|
188
|
+
### Namespaced routes:
|
189
|
+
|
190
|
+
```ruby
|
191
|
+
router = Lotus::Router.new
|
192
|
+
router.namespace 'animals' do
|
193
|
+
namespace 'mammals' do
|
194
|
+
get '/cats', to: ->(env) { [200, {}, ['Meow!']] }, as: :cats
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
# or
|
199
|
+
|
200
|
+
router.get '/cats', prefix: '/animals/mammals', to:->(env) { [200, {}, ['Meow!']] }, as: :cats
|
201
|
+
|
202
|
+
# and it generates:
|
203
|
+
|
204
|
+
router.path(:animals_mammals_cats) # => "/animals/mammals/cats"
|
205
|
+
```
|
206
|
+
|
207
|
+
|
208
|
+
|
209
|
+
### Duck typed endpoints:
|
210
|
+
|
211
|
+
Everything that responds to `#call` is invoked as it is:
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
router = Lotus::Router.new
|
215
|
+
router.get '/lotus', to: ->(env) { [200, {}, ['Hello from Lotus!']] }
|
216
|
+
router.get '/middleware', to: Middleware
|
217
|
+
router.get '/rack-app', to: RackApp.new
|
218
|
+
router.get '/method', to: ActionControllerSubclass.action(:new)
|
219
|
+
```
|
220
|
+
|
221
|
+
|
222
|
+
If it's a string, it tries to instantiate a class from it:
|
223
|
+
|
224
|
+
```ruby
|
225
|
+
class RackApp
|
226
|
+
def call(env)
|
227
|
+
# ...
|
228
|
+
end
|
229
|
+
end
|
230
|
+
|
231
|
+
router = Lotus::Router.new
|
232
|
+
router.get '/lotus', to: 'rack_app' # it will map to RackApp.new
|
233
|
+
```
|
234
|
+
|
235
|
+
It also supports Controller + Action syntax:
|
236
|
+
|
237
|
+
```ruby
|
238
|
+
class FlowersController
|
239
|
+
class Index
|
240
|
+
def call(env)
|
241
|
+
# ...
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
router = Lotus::Router.new
|
247
|
+
router.get '/flowers', to: 'flowers#index' # it will map to FlowersController::Index.new
|
248
|
+
```
|
249
|
+
|
250
|
+
|
251
|
+
|
252
|
+
### Implicit Not Found (404):
|
253
|
+
|
254
|
+
```ruby
|
255
|
+
router = Lotus::Router.new
|
256
|
+
router.call(Rack::MockRequest.env_for('/unknown')).status # => 404
|
257
|
+
```
|
258
|
+
|
259
|
+
|
260
|
+
|
261
|
+
### RESTful Resource:
|
262
|
+
|
263
|
+
```ruby
|
264
|
+
router = Lotus::Router.new
|
265
|
+
router.resource 'identity'
|
266
|
+
```
|
267
|
+
|
268
|
+
It will map:
|
269
|
+
|
270
|
+
<table>
|
271
|
+
<tr>
|
272
|
+
<th>Verb</th>
|
273
|
+
<th>Path</th>
|
274
|
+
<th>Action</th>
|
275
|
+
<th>Name</th>
|
276
|
+
<th>Named Route</th>
|
277
|
+
</tr>
|
278
|
+
<tr>
|
279
|
+
<td>GET</td>
|
280
|
+
<td>/identity</td>
|
281
|
+
<td>IdentityController::Show</td>
|
282
|
+
<td>:show</td>
|
283
|
+
<td>:identity</td>
|
284
|
+
</tr>
|
285
|
+
<tr>
|
286
|
+
<td>GET</td>
|
287
|
+
<td>/identity/new</td>
|
288
|
+
<td>IdentityController::New</td>
|
289
|
+
<td>:new</td>
|
290
|
+
<td>:new_identity</td>
|
291
|
+
</tr>
|
292
|
+
<tr>
|
293
|
+
<td>POST</td>
|
294
|
+
<td>/identity</td>
|
295
|
+
<td>IdentityController::Create</td>
|
296
|
+
<td>:create</td>
|
297
|
+
<td>:identity</td>
|
298
|
+
</tr>
|
299
|
+
<tr>
|
300
|
+
<td>GET</td>
|
301
|
+
<td>/identity/edit</td>
|
302
|
+
<td>IdentityController::Edit</td>
|
303
|
+
<td>:edit</td>
|
304
|
+
<td>:edit_identity</td>
|
305
|
+
</tr>
|
306
|
+
<tr>
|
307
|
+
<td>PATCH</td>
|
308
|
+
<td>/identity</td>
|
309
|
+
<td>IdentityController::Update</td>
|
310
|
+
<td>:update</td>
|
311
|
+
<td>:identity</td>
|
312
|
+
</tr>
|
313
|
+
<tr>
|
314
|
+
<td>DELETE</td>
|
315
|
+
<td>/identity</td>
|
316
|
+
<td>IdentityController::Destroy</td>
|
317
|
+
<td>:destroy</td>
|
318
|
+
<td>:identity</td>
|
319
|
+
</tr>
|
320
|
+
</table>
|
321
|
+
|
322
|
+
If you don't need all the default endpoints, just do:
|
323
|
+
|
324
|
+
```ruby
|
325
|
+
router = Lotus::Router.new
|
326
|
+
router.resource 'identity', only: [:edit, :update]
|
327
|
+
|
328
|
+
# which is equivalent to:
|
329
|
+
|
330
|
+
router.resource 'identity', except: [:show, :new, :create, :destroy]
|
331
|
+
```
|
332
|
+
|
333
|
+
|
334
|
+
If you need extra endpoints:
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
router = Lotus::Router.new
|
338
|
+
router.resource 'identity' do
|
339
|
+
member do
|
340
|
+
get '/avatar' # maps to IdentityController::Avatar
|
341
|
+
end
|
342
|
+
|
343
|
+
collection do
|
344
|
+
get '/authorizations' # maps to IdentityController::Authorizations
|
345
|
+
end
|
346
|
+
end
|
347
|
+
|
348
|
+
router.path(:avatar_identity) # => /identity/avatar
|
349
|
+
router.path(:authorizations_identity) # => /identity/authorizations
|
350
|
+
```
|
351
|
+
|
352
|
+
|
353
|
+
|
354
|
+
### RESTful Resources:
|
355
|
+
|
356
|
+
```ruby
|
357
|
+
router = Lotus::Router.new
|
358
|
+
router.resources 'flowers'
|
359
|
+
```
|
360
|
+
|
361
|
+
It will map:
|
362
|
+
|
363
|
+
<table>
|
364
|
+
<tr>
|
365
|
+
<th>Verb</th>
|
366
|
+
<th>Path</th>
|
367
|
+
<th>Action</th>
|
368
|
+
<th>Name</th>
|
369
|
+
<th>Named Route</th>
|
370
|
+
</tr>
|
371
|
+
<tr>
|
372
|
+
<td>GET</td>
|
373
|
+
<td>/flowers</td>
|
374
|
+
<td>FlowersController::Index</td>
|
375
|
+
<td>:index</td>
|
376
|
+
<td>:flowers</td>
|
377
|
+
</tr>
|
378
|
+
<tr>
|
379
|
+
<td>GET</td>
|
380
|
+
<td>/flowers/:id</td>
|
381
|
+
<td>FlowersController::Show</td>
|
382
|
+
<td>:show</td>
|
383
|
+
<td>:flowers</td>
|
384
|
+
</tr>
|
385
|
+
<tr>
|
386
|
+
<td>GET</td>
|
387
|
+
<td>/flowers/new</td>
|
388
|
+
<td>FlowersController::New</td>
|
389
|
+
<td>:new</td>
|
390
|
+
<td>:new_flowers</td>
|
391
|
+
</tr>
|
392
|
+
<tr>
|
393
|
+
<td>POST</td>
|
394
|
+
<td>/flowers</td>
|
395
|
+
<td>FlowersController::Create</td>
|
396
|
+
<td>:create</td>
|
397
|
+
<td>:flowers</td>
|
398
|
+
</tr>
|
399
|
+
<tr>
|
400
|
+
<td>GET</td>
|
401
|
+
<td>/flowers/:id/edit</td>
|
402
|
+
<td>FlowersController::Edit</td>
|
403
|
+
<td>:edit</td>
|
404
|
+
<td>:edit_flowers</td>
|
405
|
+
</tr>
|
406
|
+
<tr>
|
407
|
+
<td>PATCH</td>
|
408
|
+
<td>/flowers/:id</td>
|
409
|
+
<td>FlowersController::Update</td>
|
410
|
+
<td>:update</td>
|
411
|
+
<td>:flowers</td>
|
412
|
+
</tr>
|
413
|
+
<tr>
|
414
|
+
<td>DELETE</td>
|
415
|
+
<td>/flowers/:id</td>
|
416
|
+
<td>FlowersController::Destroy</td>
|
417
|
+
<td>:destroy</td>
|
418
|
+
<td>:flowers</td>
|
419
|
+
</tr>
|
420
|
+
</table>
|
421
|
+
|
422
|
+
|
423
|
+
```ruby
|
424
|
+
router.path(:flowers) # => /flowers
|
425
|
+
router.path(:flowers, id: 23) # => /flowers/23
|
426
|
+
router.path(:edit_flowers, id: 23) # => /flowers/23/edit
|
427
|
+
```
|
428
|
+
|
429
|
+
|
430
|
+
|
431
|
+
If you don't need all the default endpoints, just do:
|
432
|
+
|
433
|
+
```ruby
|
434
|
+
router = Lotus::Router.new
|
435
|
+
router.resources 'flowers', only: [:new, :create, :show]
|
436
|
+
|
437
|
+
# which is equivalent to:
|
438
|
+
|
439
|
+
router.resources 'flowers', except: [:index, :edit, :update, :destroy]
|
440
|
+
```
|
441
|
+
|
442
|
+
|
443
|
+
If you need extra endpoints:
|
444
|
+
|
445
|
+
```ruby
|
446
|
+
router = Lotus::Router.new
|
447
|
+
router.resources 'flowers' do
|
448
|
+
member do
|
449
|
+
get '/toggle' # maps to FlowersController::Toggle
|
450
|
+
end
|
451
|
+
collection do
|
452
|
+
get '/search' # maps to FlowersController::Search
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
router.path(:toggle_flowers, id: 23) # => /flowers/23/toggle
|
457
|
+
router.path(:search_flowers) # => /flowers/search
|
458
|
+
```
|
459
|
+
|
460
|
+
## Testing
|
461
|
+
|
462
|
+
```ruby
|
463
|
+
require 'lotus/router'
|
464
|
+
require 'rack/request'
|
465
|
+
|
466
|
+
router = Lotus::Router.new do
|
467
|
+
get '/', to: ->(env) { [200, {}, ['Hi!']] }
|
468
|
+
end
|
469
|
+
|
470
|
+
app = Rack::MockRequest.new(router)
|
471
|
+
app.get('/') # => #<Rack::MockResponse:0x007fc4540dc238 ...>
|
472
|
+
```
|
473
|
+
|
474
|
+
## Versioning
|
475
|
+
|
476
|
+
__Lotus::Router__ uses [Semantic Versioning 2.0.0](http://semver.org)
|
22
477
|
|
23
478
|
## Contributing
|
24
479
|
|
25
|
-
1. Fork it
|
480
|
+
1. Fork it
|
26
481
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
482
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
28
483
|
4. Push to the branch (`git push origin my-new-feature`)
|
29
484
|
5. Create new Pull Request
|
485
|
+
|
486
|
+
## Acknowledgements
|
487
|
+
|
488
|
+
Thanks to Joshua Hull ([@joshbuddy](https://github.com/joshbuddy)) for his
|
489
|
+
[http_router](http://rubygems.org/gems/http_router).
|
490
|
+
|
491
|
+
## Copyright
|
492
|
+
|
493
|
+
Copyright 2014 Luca Guidi – Released under MIT License
|