her 0.3.6 → 0.3.7
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.
- data/.gitignore +1 -0
- data/CONTRIBUTING.md +26 -0
- data/FEATURES.md +296 -0
- data/MIDDLEWARE.md +183 -0
- data/README.md +20 -482
- data/TESTING.md +88 -0
- data/her.gemspec +2 -2
- data/lib/her/api.rb +2 -2
- data/lib/her/collection.rb +3 -3
- data/lib/her/middleware/first_level_parse_json.rb +1 -1
- data/lib/her/model/http.rb +5 -1
- data/lib/her/model/orm.rb +12 -0
- data/lib/her/version.rb +1 -1
- data/spec/collection_spec.rb +27 -0
- data/spec/middleware/first_level_parse_json_spec.rb +12 -5
- data/spec/model/orm_spec.rb +25 -0
- metadata +15 -9
data/README.md
CHANGED
@@ -12,10 +12,6 @@ gem "her"
|
|
12
12
|
|
13
13
|
That’s it!
|
14
14
|
|
15
|
-
## Upgrade
|
16
|
-
|
17
|
-
Please see the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) file for backward compability issues.
|
18
|
-
|
19
15
|
## Usage
|
20
16
|
|
21
17
|
First, you have to define which API your models will be bound to. For example, with Rails, you would create a new `config/initializers/her.rb` file with these lines:
|
@@ -37,7 +33,7 @@ class User
|
|
37
33
|
end
|
38
34
|
```
|
39
35
|
|
40
|
-
After that, using Her is very similar to many
|
36
|
+
After that, using Her is very similar to many ActiveRecord-like ORMs:
|
41
37
|
|
42
38
|
```ruby
|
43
39
|
User.all
|
@@ -60,502 +56,41 @@ User.find(1)
|
|
60
56
|
# PUT https://api.example.com/users/1 with the data and return+update the User object
|
61
57
|
```
|
62
58
|
|
63
|
-
You can look into the `examples` directory for sample applications using Her.
|
64
|
-
|
65
|
-
## Middleware
|
66
|
-
|
67
|
-
Since Her relies on [Faraday](https://github.com/technoweenie/faraday) to send HTTP requests, you can choose the middleware used to handle requests and responses. Using the block in the `setup` call, you have access to Faraday’s `connection` object and are able to customize the middleware stack used on each request and response.
|
68
|
-
|
69
|
-
### Authentication
|
70
|
-
|
71
|
-
Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `connection` block, we can add it to the middleware stack.
|
72
|
-
|
73
|
-
For example, to add a API token header to your requests in a Rails application, you would do something like this:
|
74
|
-
|
75
|
-
```ruby
|
76
|
-
# app/controllers/application_controller.rb
|
77
|
-
class ApplicationController < ActionController::Base
|
78
|
-
around_filter :do_with_authenticated_user
|
79
|
-
|
80
|
-
def as_authenticated_user
|
81
|
-
Thread.current[:my_api_token] = session[:my_api_token]
|
82
|
-
begin
|
83
|
-
yield
|
84
|
-
ensure
|
85
|
-
Thread.current[:my_access_token] = nil
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# lib/my_token_authentication.rb
|
91
|
-
class MyTokenAuthentication < Faraday::Middleware
|
92
|
-
def initialize(app, options={})
|
93
|
-
@app = app
|
94
|
-
end
|
95
|
-
|
96
|
-
def call(env)
|
97
|
-
env[:request_headers]["X-API-Token"] = Thread.current[:my_api_token] if Thread.current[:my_api_token].present?
|
98
|
-
@app.call(env)
|
99
|
-
end
|
100
|
-
end
|
59
|
+
You can look into the `examples` directory for sample applications using Her. For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her).
|
101
60
|
|
102
|
-
|
103
|
-
require "lib/my_token_authentication"
|
104
|
-
|
105
|
-
Her::API.setup :url => "https://api.example.com" do |connection|
|
106
|
-
connection.use MyTokenAuthentication
|
107
|
-
connection.use Faraday::Request::UrlEncoded
|
108
|
-
connection.use Her::Middleware::DefaultParseJSON
|
109
|
-
connection.use Faraday::Adapter::NetHttp
|
110
|
-
end
|
111
|
-
```
|
112
|
-
|
113
|
-
Now, each HTTP request made by Her will have the `X-API-Token` header.
|
114
|
-
|
115
|
-
### Parsing JSON data
|
116
|
-
|
117
|
-
By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
|
118
|
-
|
119
|
-
```javascript
|
120
|
-
// The response of GET /users/1
|
121
|
-
{ "id" : 1, "name" : "Tobias Fünke" }
|
122
|
-
|
123
|
-
// The response of GET /users
|
124
|
-
[{ "id" : 1, "name" : "Tobias Fünke" }]
|
125
|
-
```
|
61
|
+
## History
|
126
62
|
|
127
|
-
|
128
|
-
|
129
|
-
```ruby
|
130
|
-
# Expects responses like:
|
131
|
-
#
|
132
|
-
# {
|
133
|
-
# "result": {
|
134
|
-
# "id": 1,
|
135
|
-
# "name": "Tobias Fünke"
|
136
|
-
# },
|
137
|
-
# "errors" => []
|
138
|
-
# }
|
139
|
-
#
|
140
|
-
class MyCustomParser < Faraday::Response::Middleware
|
141
|
-
def on_complete(env)
|
142
|
-
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
143
|
-
env[:body] = {
|
144
|
-
:data => json[:result],
|
145
|
-
:errors => json[:errors],
|
146
|
-
:metadata => json[:metadata]
|
147
|
-
}
|
148
|
-
end
|
149
|
-
end
|
150
|
-
|
151
|
-
Her::API.setup :url => "https://api.example.com" do |connection|
|
152
|
-
connection.use Faraday::Request::UrlEncoded
|
153
|
-
connection.use MyCustomParser
|
154
|
-
connection.use Faraday::Adapter::NetHttp
|
155
|
-
end
|
156
|
-
```
|
157
|
-
|
158
|
-
### OAuth
|
159
|
-
|
160
|
-
Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
|
161
|
-
|
162
|
-
In your Gemfile:
|
163
|
-
|
164
|
-
```ruby
|
165
|
-
gem "her"
|
166
|
-
gem "faraday_middleware"
|
167
|
-
gem "simple_oauth"
|
168
|
-
```
|
169
|
-
|
170
|
-
In your Ruby code:
|
171
|
-
|
172
|
-
```ruby
|
173
|
-
# Create an application on `https://dev.twitter.com/apps` to set these values
|
174
|
-
TWITTER_CREDENTIALS = {
|
175
|
-
:consumer_key => "",
|
176
|
-
:consumer_secret => "",
|
177
|
-
:token => "",
|
178
|
-
:token_secret => ""
|
179
|
-
}
|
180
|
-
|
181
|
-
Her::API.setup :url => "https://api.twitter.com/1/" do |connection|
|
182
|
-
connection.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
|
183
|
-
connection.use Faraday::Request::UrlEncoded
|
184
|
-
connection.use Her::Middleware::DefaultParseJSON
|
185
|
-
connection.use Faraday::Adapter::NetHttp
|
186
|
-
end
|
187
|
-
|
188
|
-
class Tweet
|
189
|
-
include Her::Model
|
190
|
-
end
|
191
|
-
|
192
|
-
@tweets = Tweet.get("/statuses/home_timeline.json")
|
193
|
-
```
|
194
|
-
|
195
|
-
See the *Authentication* middleware section for an example of how to pass different credentials based on the current user.
|
196
|
-
|
197
|
-
### Caching
|
198
|
-
|
199
|
-
Again, using the `faraday_middleware` makes it very easy to cache requests and responses:
|
200
|
-
|
201
|
-
In your Gemfile:
|
202
|
-
|
203
|
-
```ruby
|
204
|
-
gem "her"
|
205
|
-
gem "faraday_middleware"
|
206
|
-
```
|
207
|
-
|
208
|
-
In your Ruby code:
|
209
|
-
|
210
|
-
```ruby
|
211
|
-
class MyCache < Hash
|
212
|
-
def read(key)
|
213
|
-
if cached = self[key]
|
214
|
-
Marshal.load(cached)
|
215
|
-
end
|
216
|
-
end
|
217
|
-
|
218
|
-
def write(key, data)
|
219
|
-
self[key] = Marshal.dump(data)
|
220
|
-
end
|
221
|
-
|
222
|
-
def fetch(key)
|
223
|
-
read(key) || yield.tap { |data| write(key, data) }
|
224
|
-
end
|
225
|
-
end
|
226
|
-
|
227
|
-
# A cache system must respond to `#write`, `#read` and `#fetch`.
|
228
|
-
# We should be probably using something like Memcached here, not a global object
|
229
|
-
$cache = MyCache.new
|
230
|
-
|
231
|
-
Her::API.setup :url => "https://api.example.com" do |connection|
|
232
|
-
connection.use Faraday::Request::UrlEncoded
|
233
|
-
connection.use FaradayMiddleware::Caching, $cache
|
234
|
-
connection.use Her::Middleware::DefaultParseJSON
|
235
|
-
connection.use Faraday::Adapter::NetHttp
|
236
|
-
end
|
237
|
-
|
238
|
-
class User
|
239
|
-
include Her::Model
|
240
|
-
end
|
241
|
-
|
242
|
-
@user = User.find(1)
|
243
|
-
# GET /users/1
|
244
|
-
|
245
|
-
@user = User.find(1)
|
246
|
-
# This request will be fetched from the cache
|
247
|
-
```
|
248
|
-
|
249
|
-
## Relationships
|
250
|
-
|
251
|
-
You can define `has_many`, `has_one` and `belongs_to` relationships in your models. The relationship data is handled in two different ways.
|
252
|
-
|
253
|
-
1. If Her finds relationship data when parsing a resource, that data will be used to create the associated model objects on the resource.
|
254
|
-
2. If no relationship data was included when parsing a resource, calling a method with the same name as the relationship will fetch the data (providing there’s an HTTP request available for it in the API).
|
255
|
-
|
256
|
-
For example:
|
257
|
-
|
258
|
-
```ruby
|
259
|
-
class User
|
260
|
-
include Her::Model
|
261
|
-
has_many :comments
|
262
|
-
has_one :role
|
263
|
-
belongs_to :organization
|
264
|
-
end
|
265
|
-
|
266
|
-
class Comment
|
267
|
-
include Her::Model
|
268
|
-
end
|
269
|
-
|
270
|
-
class Role
|
271
|
-
include Her::Model
|
272
|
-
end
|
273
|
-
|
274
|
-
class Organization
|
275
|
-
include Her::Model
|
276
|
-
end
|
277
|
-
```
|
278
|
-
|
279
|
-
If there’s relationship data in the resource, no extra HTTP request is made when calling the `#comments` method and an array of resources is returned:
|
280
|
-
|
281
|
-
```ruby
|
282
|
-
@user = User.find(1)
|
283
|
-
# {
|
284
|
-
# :data => {
|
285
|
-
# :id => 1,
|
286
|
-
# :name => "George Michael Bluth",
|
287
|
-
# :comments => [
|
288
|
-
# { :id => 1, :text => "Foo" },
|
289
|
-
# { :id => 2, :text => "Bar" }
|
290
|
-
# ],
|
291
|
-
# :role => { :id => 1, :name => "Admin" },
|
292
|
-
# :organization => { :id => 2, :name => "Bluth Company" }
|
293
|
-
# }
|
294
|
-
# }
|
295
|
-
@user.comments
|
296
|
-
# [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
|
297
|
-
@user.role
|
298
|
-
# #<Role id=1 name="Admin">
|
299
|
-
@user.organization
|
300
|
-
# #<Organization id=2 name="Bluth Company">
|
301
|
-
```
|
302
|
-
|
303
|
-
If there’s no relationship data in the resource, Her makes a HTTP request to retrieve the data.
|
304
|
-
|
305
|
-
```ruby
|
306
|
-
@user = User.find(1)
|
307
|
-
# { :data => { :id => 1, :name => "George Michael Bluth", :organization_id => 2 }}
|
308
|
-
|
309
|
-
# has_many relationship:
|
310
|
-
@user.comments
|
311
|
-
# GET /users/1/comments
|
312
|
-
# [#<Comment id=1>, #<Comment id=2>]
|
313
|
-
|
314
|
-
# has_one relationship:
|
315
|
-
@user.role
|
316
|
-
# GET /users/1/role
|
317
|
-
# #<Role id=1>
|
318
|
-
|
319
|
-
# belongs_to relationship:
|
320
|
-
@user.organization
|
321
|
-
# (the organization id comes from :organization_id, by default)
|
322
|
-
# GET /organizations/2
|
323
|
-
# #<Organization id=2>
|
324
|
-
```
|
63
|
+
I told myself a few months ago that it would be great to build a gem to replace Rails’ [ActiveResource](http://api.rubyonrails.org/classes/ActiveResource/Base.html) since it was barely maintained, lacking features and hard to extend/customize. I had built a few of these REST-powered ORMs for client projects before but I decided I wanted to write one for myself that I could release as an open-source project.
|
325
64
|
|
326
|
-
|
65
|
+
Most of Her’s core codebase was written on a Saturday morning ([first commit](https://github.com/remiprev/her/commit/689d8e88916dc2ad258e69a2a91a283f061cbef2) at 7am!) while I was visiting my girlfiend’s family in [Ayer’s Cliff](https://en.wikipedia.org/wiki/Ayer%27s_Cliff).
|
327
66
|
|
328
|
-
##
|
329
|
-
|
330
|
-
You can add *before* and *after* hooks to your models that are triggered on specific actions (`save`, `update`, `create`, `destroy`):
|
331
|
-
|
332
|
-
```ruby
|
333
|
-
class User
|
334
|
-
include Her::Model
|
335
|
-
before_save :set_internal_id
|
336
|
-
|
337
|
-
def set_internal_id
|
338
|
-
self.internal_id = 42 # Will be passed in the HTTP request
|
339
|
-
end
|
340
|
-
end
|
341
|
-
|
342
|
-
@user = User.create(:fullname => "Tobias Fünke")
|
343
|
-
# POST /users&fullname=Tobias+Fünke&internal_id=42
|
344
|
-
```
|
345
|
-
|
346
|
-
## Custom requests
|
347
|
-
|
348
|
-
You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
|
349
|
-
|
350
|
-
```ruby
|
351
|
-
class User
|
352
|
-
include Her::Model
|
353
|
-
custom_get :popular, :unpopular
|
354
|
-
custom_post :from_default
|
355
|
-
end
|
356
|
-
|
357
|
-
User.popular
|
358
|
-
# GET /users/popular
|
359
|
-
# [#<User id=1>, #<User id=2>]
|
360
|
-
|
361
|
-
User.unpopular
|
362
|
-
# GET /users/unpopular
|
363
|
-
# [#<User id=3>, #<User id=4>]
|
364
|
-
|
365
|
-
User.from_default(:name => "Maeby Fünke")
|
366
|
-
# POST /users/from_default?name=Maeby+Fünke
|
367
|
-
# #<User id=5 name="Maeby Fünke">
|
368
|
-
```
|
369
|
-
|
370
|
-
You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
|
371
|
-
|
372
|
-
```ruby
|
373
|
-
class User
|
374
|
-
include Her::Model
|
375
|
-
end
|
376
|
-
|
377
|
-
User.get(:popular)
|
378
|
-
# GET /users/popular
|
379
|
-
# [#<User id=1>, #<User id=2>]
|
380
|
-
|
381
|
-
User.get(:single_best)
|
382
|
-
# GET /users/single_best
|
383
|
-
# #<User id=1>
|
384
|
-
```
|
385
|
-
|
386
|
-
Also, `get_collection` (which maps the returned data to a collection of resources), `get_resource` (which maps the returned data to a single resource) or `get_raw` (which yields the parsed data return from the HTTP request) can also be used. Other HTTP methods are supported (`post_raw`, `put_resource`, etc.).
|
387
|
-
|
388
|
-
```ruby
|
389
|
-
class User
|
390
|
-
include Her::Model
|
391
|
-
|
392
|
-
def self.popular
|
393
|
-
get_collection(:popular)
|
394
|
-
end
|
395
|
-
|
396
|
-
def self.total
|
397
|
-
get_raw(:stats) do |parsed_data|
|
398
|
-
parsed_data[:data][:total_users]
|
399
|
-
end
|
400
|
-
end
|
401
|
-
end
|
402
|
-
|
403
|
-
User.popular
|
404
|
-
# GET /users/popular
|
405
|
-
# [#<User id=1>, #<User id=2>]
|
406
|
-
User.total
|
407
|
-
# GET /users/stats
|
408
|
-
# => 42
|
409
|
-
```
|
410
|
-
|
411
|
-
You can also use full request paths (with strings instead of symbols).
|
412
|
-
|
413
|
-
```ruby
|
414
|
-
class User
|
415
|
-
include Her::Model
|
416
|
-
end
|
417
|
-
|
418
|
-
User.get("/users/popular")
|
419
|
-
# GET /users/popular
|
420
|
-
# [#<User id=1>, #<User id=2>]
|
421
|
-
```
|
422
|
-
|
423
|
-
## Custom paths
|
424
|
-
|
425
|
-
You can define custom HTTP paths for your models:
|
426
|
-
|
427
|
-
```ruby
|
428
|
-
class User
|
429
|
-
include Her::Model
|
430
|
-
collection_path "/hello_users/:id"
|
431
|
-
end
|
432
|
-
|
433
|
-
@user = User.find(1)
|
434
|
-
# GET /hello_users/1
|
435
|
-
```
|
436
|
-
|
437
|
-
You can also include custom variables in your paths:
|
438
|
-
|
439
|
-
```ruby
|
440
|
-
class User
|
441
|
-
include Her::Model
|
442
|
-
collection_path "/organizations/:organization_id/users"
|
443
|
-
end
|
444
|
-
|
445
|
-
@user = User.find(1, :_organization_id => 2)
|
446
|
-
# GET /organizations/2/users/1
|
447
|
-
|
448
|
-
@user = User.all(:_organization_id => 2)
|
449
|
-
# GET /organizations/2/users
|
450
|
-
|
451
|
-
@user = User.new(:fullname => "Tobias Fünke", :organization_id => 2)
|
452
|
-
@user.save
|
453
|
-
# POST /organizations/2/users
|
454
|
-
```
|
455
|
-
|
456
|
-
## Multiple APIs
|
457
|
-
|
458
|
-
It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
|
459
|
-
|
460
|
-
```ruby
|
461
|
-
# config/initializers/her.rb
|
462
|
-
$my_api = Her::API.new
|
463
|
-
$my_api.setup :url => "https://my_api.example.com" do |connection|
|
464
|
-
connection.use Faraday::Request::UrlEncoded
|
465
|
-
connection.use Her::Middleware::DefaultParseJSON
|
466
|
-
connection.use Faraday::Adapter::NetHttp
|
467
|
-
end
|
468
|
-
|
469
|
-
$other_api = Her::API.new
|
470
|
-
$other_api.setup :url => "https://other_api.example.com" do |connection|
|
471
|
-
connection.use Faraday::Request::UrlEncoded
|
472
|
-
connection.use Her::Middleware::DefaultParseJSON
|
473
|
-
connection.use Faraday::Adapter::NetHttp
|
474
|
-
end
|
475
|
-
```
|
476
|
-
|
477
|
-
You can then define which API a model will use:
|
478
|
-
|
479
|
-
```ruby
|
480
|
-
class User
|
481
|
-
include Her::Model
|
482
|
-
uses_api $my_api
|
483
|
-
end
|
484
|
-
|
485
|
-
class Category
|
486
|
-
include Her::Model
|
487
|
-
uses_api $other_api
|
488
|
-
end
|
489
|
-
|
490
|
-
User.all
|
491
|
-
# GET https://my_api.example.com/users
|
492
|
-
|
493
|
-
Category.all
|
494
|
-
# GET https://other_api.example.com/categories
|
495
|
-
```
|
67
|
+
## Middleware
|
496
68
|
|
497
|
-
|
69
|
+
See [MIDDLEWARE.md](https://github.com/remiprev/her/blob/master/MIDDLEWARE.md) to learn how to use [Faraday](https://github.com/technoweenie/faraday)’s middleware to customize how Her handles HTTP requests and responses.
|
498
70
|
|
499
|
-
|
71
|
+
## Features
|
500
72
|
|
501
|
-
|
502
|
-
ssl_options = { :ca_path => "/usr/lib/ssl/certs" }
|
503
|
-
Her::API.setup :url => "https://api.example.com", :ssl => ssl_options do |connection|
|
504
|
-
connection.use Faraday::Request::UrlEncoded
|
505
|
-
connection.use Her::Middleware::DefaultParseJSON
|
506
|
-
connection.use Faraday::Adapter::NetHttp
|
507
|
-
end
|
508
|
-
```
|
73
|
+
See [FEATURES.md](https://github.com/remiprev/her/blob/master/FEATURES.md) to learn about Her’s advanced features.
|
509
74
|
|
510
75
|
## Testing
|
511
76
|
|
512
|
-
|
77
|
+
See [TESTING.md](https://github.com/remiprev/her/blob/master/TESTING.md) to learn how to test models using stubbed HTTP requests.
|
513
78
|
|
514
|
-
|
515
|
-
# app/models/post.rb
|
516
|
-
class Post
|
517
|
-
include Her::Model
|
518
|
-
custom_get :popular
|
519
|
-
end
|
79
|
+
## Upgrade
|
520
80
|
|
521
|
-
|
522
|
-
|
523
|
-
|
524
|
-
Her::API.setup :url => "http://api.example.com" do |connection|
|
525
|
-
connection.use Her::Middleware::FirstLevelParseJSON
|
526
|
-
connection.use Faraday::Request::UrlEncoded
|
527
|
-
connection.adapter :test do |stub|
|
528
|
-
stub.get("/users/popular") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] }
|
529
|
-
end
|
530
|
-
end
|
531
|
-
end
|
532
|
-
|
533
|
-
describe ".popular" do
|
534
|
-
it "should fetch all popular posts" do
|
535
|
-
@posts = Post.popular
|
536
|
-
@posts.length.should == 2
|
537
|
-
end
|
538
|
-
end
|
539
|
-
end
|
540
|
-
```
|
81
|
+
See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for backward compability issues.
|
82
|
+
|
83
|
+
## Her IRL
|
541
84
|
|
542
|
-
|
85
|
+
Most projects I know that use Her are internal or private projects but here’s a list of public ones:
|
543
86
|
|
544
|
-
*
|
545
|
-
* Better API documentation (using YARD)
|
87
|
+
* [tumbz](https://github.com/remiprev/tumbz)
|
546
88
|
|
547
89
|
## Contribute
|
548
90
|
|
549
91
|
Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues).
|
550
92
|
|
551
|
-
|
552
|
-
|
553
|
-
* Fork the repository
|
554
|
-
* Implement your feature or fix
|
555
|
-
* Add examples that describe it (in the `spec` directory)
|
556
|
-
* Make sure `bundle exec rake spec` passes after your modifications
|
557
|
-
* Commit (bonus points for doing it in a `feature-*` branch)
|
558
|
-
* Send a pull request!
|
93
|
+
See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices.
|
559
94
|
|
560
95
|
### Contributors
|
561
96
|
|
@@ -569,6 +104,9 @@ These fine folks helped with Her:
|
|
569
104
|
* [@simonprevost](https://github.com/simonprevost)
|
570
105
|
* [@jmlacroix](https://github.com/jmlacroix)
|
571
106
|
* [@thomsbg](https://github.com/thomsbg)
|
107
|
+
* [@calmyournerves](https://github.com/calmyournerves)
|
108
|
+
* [@luflux](https://github.com/luxflux)
|
109
|
+
* [@simonc](https://github.com/simonc)
|
572
110
|
|
573
111
|
## License
|
574
112
|
|