her 0.3.6 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
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 ActiveModel-like ORMs:
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
- # config/initializers/her.rb
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
- However, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three keys: `data`, `errors` and `metadata`. The following code uses a custom middleware to parse the JSON data:
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
- Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
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
- ## Hooks
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
- ## SSL
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
- When initializing `Her::API`, you can pass any parameter supported by `Faraday.new`. So [to use HTTPS](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates), you can use Faraday’s `:ssl` option.
71
+ ## Features
500
72
 
501
- ```ruby
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
- Using Faraday stubbing feature, it’s very easy to write tests for our models. For example, using [RSpec](https://github.com/rspec/rspec-core):
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
- ```ruby
515
- # app/models/post.rb
516
- class Post
517
- include Her::Model
518
- custom_get :popular
519
- end
79
+ ## Upgrade
520
80
 
521
- # spec/models/post.rb
522
- describe Post do
523
- before do
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
- ## Things to be done
85
+ Most projects I know that use Her are internal or private projects but here’s a list of public ones:
543
86
 
544
- * Better error handling
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
- ### How to contribute
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