Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 471 lines (344 sloc) 13.07 kB
871ef96 @remiprev Add README
authored
1 # Her
2
6bd587d @remiprev Improve README, yay!
authored
3 Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects. It is designed to build applications that are powered by a RESTful API instead of a database.
871ef96 @remiprev Add README
authored
4
b93a382 @remiprev Move build status image
authored
5 [![Build Status](https://secure.travis-ci.org/remiprev/her.png)](http://travis-ci.org/remiprev/her)
6
871ef96 @remiprev Add README
authored
7 ## Installation
8
9 In your Gemfile, add:
10
60f209e @remiprev Update README
authored
11 ```ruby
12 gem "her"
13 ```
871ef96 @remiprev Add README
authored
14
6878101 @remiprev Update documentation with new API code
authored
15 That’s it!
16
d907ddd @remiprev Bump version to 0.2.4, add note about UPGRADE.md
authored
17 ## Upgrade
18
19 Please see the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) file for backward compability issues.
20
871ef96 @remiprev Add README
authored
21 ## Usage
22
53744d4 @remiprev Do not set any default middleware
authored
23 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:
871ef96 @remiprev Add README
authored
24
25 ```ruby
cdeb59f @remiprev Add Multiple APIs section in README
authored
26 # config/initializers/her.rb
53744d4 @remiprev Do not set any default middleware
authored
27 Her::API.setup :base_uri => "https://api.example.com" do |builder|
28 builder.use Faraday::Request::UrlEncoded
29 builder.use Her::Middleware::DefaultParseJSON
30 builder.use Faraday::Adapter::NetHttp
31 end
871ef96 @remiprev Add README
authored
32 ```
33
6878101 @remiprev Update documentation with new API code
authored
34 And then to add the ORM behavior to a class, you just have to include `Her::Model` in it:
871ef96 @remiprev Add README
authored
35
36 ```ruby
37 class User
38 include Her::Model
39 end
40 ```
41
42 After that, using Her is very similar to many ActiveModel-like ORMs:
43
44 ```ruby
a6e9967 @remiprev Update README with more examples
authored
45 User.all
cdeb59f @remiprev Add Multiple APIs section in README
authored
46 # GET https://api.example.com/users and return an array of User objects
a6e9967 @remiprev Update README with more examples
authored
47
48 User.find(1)
cdeb59f @remiprev Add Multiple APIs section in README
authored
49 # GET https://api.example.com/users/1 and return a User object
a6e9967 @remiprev Update README with more examples
authored
50
51 @user = User.create(:fullname => "Tobias Fünke")
bc5779a @remiprev Fix typo
authored
52 # POST "https://api.example.com/users" with the data and return a User object
a6e9967 @remiprev Update README with more examples
authored
53
54 @user = User.new(:fullname => "Tobias Fünke")
55 @user.occupation = "actor"
56 @user.save
cdeb59f @remiprev Add Multiple APIs section in README
authored
57 # POST https://api.example.com/users with the data and return a User object
a6e9967 @remiprev Update README with more examples
authored
58
59 @user = User.find(1)
60 @user.fullname = "Lindsay Fünke"
61 @user.save
cdeb59f @remiprev Add Multiple APIs section in README
authored
62 # PUT https://api.example.com/users/1 with the data and return+update the User object
871ef96 @remiprev Add README
authored
63 ```
60f209e @remiprev Update README
authored
64
fd7dbca @remiprev Fix typo in README
authored
65 ## Middleware
3b3c7ac @remiprev Improve middleware handling on API#setup
authored
66
53744d4 @remiprev Do not set any default middleware
authored
67 Since Her relies on [Faraday](https://github.com/technoweenie/faraday) to send HTTP requests, you can add additional middleware to handle requests and responses. Using the block in the `setup` call, you have access to Faraday’s `builder` object and are able to customize the middleware stack used on each request and response.
3b3c7ac @remiprev Improve middleware handling on API#setup
authored
68
69 ### Authentication
70
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
71 Her doesn’t support any kind of authentication. However, it’s very easy to implement one with a request middleware. Using the builder block, we add it to the default list of middleware.
3b3c7ac @remiprev Improve middleware handling on API#setup
authored
72
73 ```ruby
74 class MyAuthentication < Faraday::Middleware
6bd587d @remiprev Improve README, yay!
authored
75 def initialize(app, options={})
76 @options = options
77 end
78
3b3c7ac @remiprev Improve middleware handling on API#setup
authored
79 def call(env)
6bd587d @remiprev Improve README, yay!
authored
80 env[:request_headers]["X-API-Token"] = @options[:token] if @options.include?(:token)
388826c @remiprev Add OAuth example in documentation
authored
81 @app.call(env)
3b3c7ac @remiprev Improve middleware handling on API#setup
authored
82 end
83 end
84
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
85 Her::API.setup :base_uri => "https://api.example.com" do |builder|
6bd587d @remiprev Improve README, yay!
authored
86 # This token could be stored in the client session
53744d4 @remiprev Do not set any default middleware
authored
87 builder.use MyAuthentication, :token => "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
88
89 builder.use Faraday::Request::UrlEncoded
90 builder.use Her::Middleware::DefaultParseJSON
91 builder.use Faraday::Adapter::NetHttp
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
92 end
3b3c7ac @remiprev Improve middleware handling on API#setup
authored
93 ```
94
95 Now, each HTTP request made by Her will have the `X-API-Token` header.
96
1154d56 @remiprev Expect first-level data to be returned by APIs
authored
97 ### Parsing JSON data
ab31348 @remiprev Add Parsing data section in README
authored
98
1154d56 @remiprev Expect first-level data to be returned by APIs
authored
99 By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
100
8f55b37 @remiprev Fix typo in README
authored
101 ```javascript
ab31348 @remiprev Add Parsing data section in README
authored
102 // The response of GET /users/1
1154d56 @remiprev Expect first-level data to be returned by APIs
authored
103 { "id" : 1, "name" : "Tobias Fünke" }
ab31348 @remiprev Add Parsing data section in README
authored
104
105 // The response of GET /users
1154d56 @remiprev Expect first-level data to be returned by APIs
authored
106 [{ "id" : 1, "name" : "Tobias Fünke" }]
ab31348 @remiprev Add Parsing data section in README
authored
107 ```
108
d0bfc9e @remiprev Use swap instead of delete/use!
authored
109 However, you can define your own parsing method, using a response middleware. The middleware is expected to set `env[:body]` to a hash with three keys: `data`, `errors` and `metadata`. The following code enables parsing JSON data and treating the result as first-level properties. Using the builder block, we then replace the default parser with our custom parser.
ab31348 @remiprev Add Parsing data section in README
authored
110
111 ```ruby
3d1da8d @remiprev Add support for custom Faraday middleware
authored
112 class MyCustomParser < Faraday::Response::Middleware
113 def on_complete(env)
490c313 @remiprev Fix typo in README
authored
114 json = MultiJson.load(env[:body], :symbolize_keys => true)
1154d56 @remiprev Expect first-level data to be returned by APIs
authored
115 env[:body] = {
6bd587d @remiprev Improve README, yay!
authored
116 :data => json[:result],
1154d56 @remiprev Expect first-level data to be returned by APIs
authored
117 :errors => json[:errors],
118 :metadata => json[:metadata]
119 }
3d1da8d @remiprev Add support for custom Faraday middleware
authored
120 end
121 end
8a5bee6 @remiprev Fix a few typos
authored
122
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
123 Her::API.setup :base_uri => "https://api.example.com" do |builder|
53744d4 @remiprev Do not set any default middleware
authored
124 builder.use Faraday::Request::UrlEncoded
125 builder.use MyCustomParser
126 builder.use Faraday::Adapter::NetHttp
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
127 end
6bd587d @remiprev Improve README, yay!
authored
128 # User.find(1) will now expect "https://api.example.com/users/1" to return something like '{ "result" => { "id": 1, "name": "Tobias Fünke" }, "errors" => [] }'
ab31348 @remiprev Add Parsing data section in README
authored
129 ```
130
388826c @remiprev Add OAuth example in documentation
authored
131 ### OAuth
132
133 Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
134
135 In your Gemfile:
136
137 ```ruby
c01ceda @remiprev Update OAuth example in README
authored
138 gem "her"
139 gem "faraday_middleware"
140 gem "simple_oauth"
388826c @remiprev Add OAuth example in documentation
authored
141 ```
142
143 In your Ruby code:
144
145 ```ruby
c01ceda @remiprev Update OAuth example in README
authored
146 # Create an application on `https://dev.twitter.com/apps` to set these values
388826c @remiprev Add OAuth example in documentation
authored
147 TWITTER_CREDENTIALS = {
148 :consumer_key => "",
149 :consumer_secret => "",
150 :token => "",
151 :token_secret => ""
152 }
153
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
154 Her::API.setup :base_uri => "https://api.twitter.com/1/" do |builder|
53744d4 @remiprev Do not set any default middleware
authored
155 builder.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
156 builder.use Faraday::Request::UrlEncoded
157 builder.use Her::Middleware::DefaultParseJSON
158 builder.use Faraday::Adapter::NetHttp
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
159 end
388826c @remiprev Add OAuth example in documentation
authored
160
161 class Tweet
162 include Her::Model
163 end
164
165 @tweets = Tweet.get("/statuses/home_timeline.json")
166 ```
167
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
168 ### Caching
169
170 Again, using the `faraday_middleware` makes it very easy to cache requests and responses:
171
172 In your Gemfile:
173
174 ```ruby
175 gem "her"
176 gem "faraday_middleware"
177 ```
178
179 In your Ruby code:
180
181 ```ruby
721b7f4 @remiprev Update caching example in README
authored
182 class MyCache < Hash
183 def read(key)
184 if cached = self[key]
185 Marshal.load(cached)
186 end
4b824f9 @remiprev Update caching example in README
authored
187 end
188
721b7f4 @remiprev Update caching example in README
authored
189 def write(key, data)
190 self[key] = Marshal.dump(data)
4b824f9 @remiprev Update caching example in README
authored
191 end
192
721b7f4 @remiprev Update caching example in README
authored
193 def fetch(key)
194 read(key) || yield.tap { |data| write(key, data) }
4b824f9 @remiprev Update caching example in README
authored
195 end
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
196 end
197
27a9a78 @remiprev Fix typo in README
authored
198 # A cache system must respond to `#write`, `#read` and `#fetch`.
199 # We should be probably using something like Memcached here, not a global object
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
200 $cache = MyCache.new
201
202 Her::API.setup :base_uri => "https://api.example.com" do |builder|
53744d4 @remiprev Do not set any default middleware
authored
203 builder.use Faraday::Request::UrlEncoded
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
204 builder.use FaradayMiddleware::Caching, $cache
53744d4 @remiprev Do not set any default middleware
authored
205 builder.use Her::Middleware::DefaultParseJSON
206 builder.use Faraday::Adapter::NetHttp
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
207 end
208
209 class User
210 include Her::Model
211 end
212
213 @user = User.find(1)
6bd587d @remiprev Improve README, yay!
authored
214 # GET /users/1
215
216 @user = User.find(1)
217 # This request will be fetched from the cache
b73abec @remiprev Improve the way we handle middleware; expose Faraday’s connection bui…
authored
218 ```
219
6878101 @remiprev Update documentation with new API code
authored
220 ## Relationships
221
6bd587d @remiprev Improve README, yay!
authored
222 You can define `has_many`, `has_one` and `belongs_to` relationships in your models. The relationship data is handled in two different ways. If there’s relationship data when parsing a resource, it will be used to create new Ruby objects.
313c487 @remiprev Add better documentation for relationships
authored
223
875f744 @remiprev Update README for belongs_to
authored
224 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).
313c487 @remiprev Add better documentation for relationships
authored
225
226 For example, with this setup:
227
6878101 @remiprev Update documentation with new API code
authored
228 ```ruby
229 class User
230 include Her::Model
231 has_many :comments
d997547 Updated readme for has_one relationship
jfcixmedia authored
232 has_one :role
875f744 @remiprev Update README for belongs_to
authored
233 belongs_to :organization
6878101 @remiprev Update documentation with new API code
authored
234 end
235
236 class Comment
237 include Her::Model
238 end
d997547 Updated readme for has_one relationship
jfcixmedia authored
239
240 class Role
241 include Her::Model
242 end
875f744 @remiprev Update README for belongs_to
authored
243
244 class Organization
245 include Her::Model
246 end
313c487 @remiprev Add better documentation for relationships
authored
247 ```
248
6bd587d @remiprev Improve README, yay!
authored
249 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:
6878101 @remiprev Update documentation with new API code
authored
250
313c487 @remiprev Add better documentation for relationships
authored
251 ```ruby
875f744 @remiprev Update README for belongs_to
authored
252 @user = User.find(1) # { :data => { :id => 1, :name => "George Michael Bluth", :comments => [{ :id => 1, :text => "Foo" }, { :id => 2, :text => "Bar" }], :role => { :id => 1, :name => "Admin" }, :organization => { :id => 2, :name => "Bluth Company" } }}
313c487 @remiprev Add better documentation for relationships
authored
253 @user.comments # => [#<Comment id=1>, #<Comment id=2>] fetched directly from @user
d997547 Updated readme for has_one relationship
jfcixmedia authored
254 @user.role # => #<Role id=1> fetched directly from @user
875f744 @remiprev Update README for belongs_to
authored
255 @user.organization # => #<Organization id=2> fetched directly from @user
6878101 @remiprev Update documentation with new API code
authored
256 ```
257
313c487 @remiprev Add better documentation for relationships
authored
258 If there’s no relationship data in the resource, an extra HTTP request (to `GET /users/1/comments`) is made when calling the `#comments` method:
259
260 ```ruby
a6e9967 @remiprev Update README with more examples
authored
261 @user = User.find(1) # { :data => { :id => 1, :name => "George Michael Bluth" }}
313c487 @remiprev Add better documentation for relationships
authored
262 @user.comments # => [#<Comment id=1>, #<Comment id=2>] fetched from /users/1/comments
263 ```
264
6bd587d @remiprev Improve README, yay!
authored
265 For `has_one` relationships, an extra HTTP request (to `GET /users/1/role`) is made when calling the `#role` method:
d997547 Updated readme for has_one relationship
jfcixmedia authored
266
267 ```ruby
268 @user = User.find(1) # { :data => { :id => 1, :name => "George Michael Bluth" }}
269 @user.role # => #<Role id=1> fetched from /users/1/role
270 ```
271
6bd587d @remiprev Improve README, yay!
authored
272 For `belongs_to` relationships, an extra HTTP request (to `GET /organizations/2`) is made when calling the `#organization` method:
875f744 @remiprev Update README for belongs_to
authored
273
274 ```ruby
275 @user = User.find(1) # { :data => { :id => 1, :name => "George Michael Bluth", :organization_id => 2 }}
276 @user.organization # => #<Organization id=2> fetched from /organizations/2
277 ```
278
6bd587d @remiprev Improve README, yay!
authored
279 However, subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests as the data has already been fetched.
313c487 @remiprev Add better documentation for relationships
authored
280
3bb8232 @remiprev Add documentation for hooks
authored
281 ## Hooks
282
283 You can add *before* and *after* hooks to your models that are triggered on specific actions (`save`, `update`, `create`, `destroy`):
284
285 ```ruby
286 class User
287 include Her::Model
288 before_save :set_internal_id
289
290 def set_internal_id
291 self.internal_id = 42 # Will be passed in the HTTP request
292 end
293 end
ee90237 @remiprev Add example for hooks
authored
294
295 @user = User.create(:fullname => "Tobias Fünke")
296 # POST /users&fullname=Tobias+Fünke&internal_id=42
3bb8232 @remiprev Add documentation for hooks
authored
297 ```
298
6878101 @remiprev Update documentation with new API code
authored
299 ## Custom requests
300
2c0c171 @remiprev Add support for custom_<method> methods
authored
301 You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
302
303 ```ruby
304 class User
305 include Her::Model
306 custom_get :popular, :unpopular
307 custom_post :from_default
308 end
309
310 User.popular # => [#<User id=1>, #<User id=2>]
311 # GET /users/popular
312
313 User.unpopular # => [#<User id=3>, #<User id=4>]
314 # GET /users/unpopular
315
316 User.from_default(:name => "Maeby Fünke") # => #<User id=5>
317 # POST /users/from_default?name=Maeby+Fünke
318 ```
319
320 You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
6878101 @remiprev Update documentation with new API code
authored
321
17d968c @remiprev Improve support for custom methods
authored
322 ```ruby
323 class User
324 include Her::Model
a7ae25f @remiprev Add support for symbols in custom requests
authored
325 end
326
37aef13 @remiprev Add support for `get`, `post`, etc. wrapper methods
authored
327 User.get(:popular) # => [#<User id=1>, #<User id=2>]
a7ae25f @remiprev Add support for symbols in custom requests
authored
328 # GET /users/popular
37aef13 @remiprev Add support for `get`, `post`, etc. wrapper methods
authored
329
330 User.get(:single_best) # => #<User id=1>
331 # GET /users/single_best
a7ae25f @remiprev Add support for symbols in custom requests
authored
332 ```
333
37aef13 @remiprev Add support for `get`, `post`, etc. wrapper methods
authored
334 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.).
a7ae25f @remiprev Add support for symbols in custom requests
authored
335
336 ```ruby
337 class User
338 include Her::Model
17d968c @remiprev Improve support for custom methods
authored
339
340 def self.popular
37aef13 @remiprev Add support for `get`, `post`, etc. wrapper methods
authored
341 get_collection(:popular)
17d968c @remiprev Improve support for custom methods
authored
342 end
6878101 @remiprev Update documentation with new API code
authored
343
17d968c @remiprev Improve support for custom methods
authored
344 def self.total
37aef13 @remiprev Add support for `get`, `post`, etc. wrapper methods
authored
345 get_raw(:stats) do |parsed_data|
17d968c @remiprev Improve support for custom methods
authored
346 parsed_data[:data][:total_users]
347 end
348 end
349 end
350
a7ae25f @remiprev Add support for symbols in custom requests
authored
351 User.popular # => [#<User id=1>, #<User id=2>]
352 User.total # => 42
17d968c @remiprev Improve support for custom methods
authored
353 ```
7f7392f @remiprev Improve README and add LICENSE
authored
354
37aef13 @remiprev Add support for `get`, `post`, etc. wrapper methods
authored
355 You can also use full request paths (with strings instead of symbols).
356
357 ```ruby
358 class User
359 include Her::Model
360 end
361
362 User.get("/users/popular") # => [#<User id=1>, #<User id=2>]
363 # GET /users/popular
364 ```
365
fca11ad @remiprev Use internal parameter to build custom paths
authored
366 ## Custom paths
367
368 You can define custom HTTP paths for your models:
369
370 ```ruby
371 class User
372 include Her::Model
373 collection_path "/hello_users/:id"
374 end
375
376 @user = User.find(1)
377 # GET /hello_users/1
378 ```
379
6bd587d @remiprev Improve README, yay!
authored
380 You can also include custom variables in your paths:
fca11ad @remiprev Use internal parameter to build custom paths
authored
381
382 ```ruby
383 class User
384 include Her::Model
6bd587d @remiprev Improve README, yay!
authored
385 collection_path "/organizations/:organization_id/users"
fca11ad @remiprev Use internal parameter to build custom paths
authored
386 end
387
388 @user = User.find(1, :_organization_id => 2)
389 # GET /organizations/2/users/1
390
391 @user = User.all(:_organization_id => 2)
392 # GET /organizations/2/users
6bd587d @remiprev Improve README, yay!
authored
393
394 @user = User.new(:fullname => "Tobias Fünke", :organization_id => 2)
395 @user.save
396 # POST /organizations/2/users
fca11ad @remiprev Use internal parameter to build custom paths
authored
397 ```
398
cdeb59f @remiprev Add Multiple APIs section in README
authored
399 ## Multiple APIs
400
401 It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
402
403 ```ruby
404 # config/initializers/her.rb
405 $my_api = Her::API.new
53744d4 @remiprev Do not set any default middleware
authored
406 $my_api.setup :base_uri => "https://my_api.example.com" do |builder|
407 builder.use Faraday::Request::UrlEncoded
408 builder.use Her::Middleware::DefaultParseJSON
409 builder.use Faraday::Adapter::NetHttp
410 end
cdeb59f @remiprev Add Multiple APIs section in README
authored
411
412 $other_api = Her::API.new
53744d4 @remiprev Do not set any default middleware
authored
413 $other_api.setup :base_uri => "https://other_api.example.com" do |builder|
414 builder.use Faraday::Request::UrlEncoded
415 builder.use Her::Middleware::DefaultParseJSON
416 builder.use Faraday::Adapter::NetHttp
417 end
cdeb59f @remiprev Add Multiple APIs section in README
authored
418 ```
419
420 You can then define which API a model will use:
421
422 ```ruby
423 class User
424 include Her::Model
425 uses_api $my_api
426 end
427
428 class Category
429 include Her::Model
430 uses_api $other_api
431 end
432
433 User.all
434 # GET https://my_api.example.com/users
435
436 Category.all
437 # GET https://other_api.example.com/categories
438 ```
439
7f7392f @remiprev Improve README and add LICENSE
authored
440 ## Things to be done
441
a6e9967 @remiprev Update README with more examples
authored
442 * Better error handling
6bd587d @remiprev Improve README, yay!
authored
443 * Better API documentation (using YARD)
7f7392f @remiprev Improve README and add LICENSE
authored
444
676a56f @remiprev Improve Contribute section in README
authored
445 ## Contribute
7f7392f @remiprev Improve README and add LICENSE
authored
446
676a56f @remiprev Improve Contribute section in README
authored
447 Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues).
448
449 ### How to contribute
450
451 * Fork the repository
452 * Implement your feature or fix
453 * Add examples that describe it (in the `spec` directory)
454 * Make sure `bundle exec rake spec` passes after your modifications
455 * Commit (bonus points for doing it in a `feature-*` branch)
456 * Send a pull request!
457
458 ### Contributors
459
460 These fine folks helped with Her:
7f9f399 @remiprev Add contributors, woohoo!
authored
461
462 * [@jfcixmedia](https://github.com/jfcixmedia)
463 * [@EtienneLem](https://github.com/EtienneLem)
a366819 @remiprev Add @rafaelss to contributors
authored
464 * [@rafaelss](https://github.com/rafaelss)
855b897 @remiprev Update contributors with @tysontate
authored
465 * [@tysontate](https://github.com/tysontate)
2163cf5 @remiprev Add Nicolas Fouché to contributors list
authored
466 * [@nfo](https://github.com/nfo)
7f7392f @remiprev Improve README and add LICENSE
authored
467
468 ## License
469
fd5bb21 @fbernier Show the correct license name in README
fbernier authored
470 Her is © 2012 [Rémi Prévost](http://exomel.com) and may be freely distributed under the [MIT license](https://github.com/remiprev/her/blob/master/LICENSE). See the `LICENSE` file.
Something went wrong with that request. Please try again.