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/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