her 0.3.7 → 0.3.8

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -56,41 +56,593 @@ User.find(1)
56
56
  # PUT https://api.example.com/users/1 with the data and return+update the User object
57
57
  ```
58
58
 
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).
59
+ ### ActiveRecord-like methods
60
60
 
61
- ## History
61
+ These are the basic ActiveRecord-like methods you can use with your models:
62
62
 
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.
63
+ ```ruby
64
+ class User
65
+ include Her::Model
66
+ end
64
67
 
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).
68
+ # Update a fetched resource
69
+ user = User.find(1)
70
+ user.fullname = "Lindsay Fünke"
71
+ # OR user.assign_attributes :fullname => "Lindsay Fünke"
72
+ user.save
73
+
74
+ # Update a resource without fetching it
75
+ User.save_existing(1, :fullname => "Lindsay Fünke")
76
+
77
+ # Destroy a fetched resource
78
+ user = User.find(1)
79
+ user.destroy
80
+
81
+ # Destroy a resource without fetching it
82
+ User.destroy_existing(1)
83
+
84
+ # Fetching a collection of resources
85
+ User.all
86
+
87
+ # Create a new resource
88
+ User.create(:fullname => "Maeby Fünke")
89
+
90
+ # Save a new resource
91
+ user = User.new(:fullname => "Maeby Fünke")
92
+ user.save
93
+ ```
94
+
95
+ 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).
66
96
 
67
97
  ## Middleware
68
98
 
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.
99
+ 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.
100
+
101
+ ### Authentication
102
+
103
+ 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.
104
+
105
+ For example, to add a API token header to your requests in a Rails application, you would do something like this:
106
+
107
+ ```ruby
108
+ # app/controllers/application_controller.rb
109
+ class ApplicationController < ActionController::Base
110
+ around_filter :do_with_authenticated_user
111
+
112
+ def do_with_authenticated_user
113
+ Thread.current[:my_api_token] = session[:my_api_token]
114
+ begin
115
+ yield
116
+ ensure
117
+ Thread.current[:my_access_token] = nil
118
+ end
119
+ end
120
+ end
121
+
122
+ # lib/my_token_authentication.rb
123
+ class MyTokenAuthentication < Faraday::Middleware
124
+ def initialize(app, options={})
125
+ @app = app
126
+ end
127
+
128
+ def call(env)
129
+ env[:request_headers]["X-API-Token"] = Thread.current[:my_api_token] if Thread.current[:my_api_token].present?
130
+ @app.call(env)
131
+ end
132
+ end
133
+
134
+ # config/initializers/her.rb
135
+ require "lib/my_token_authentication"
136
+
137
+ Her::API.setup :url => "https://api.example.com" do |connection|
138
+ connection.use MyTokenAuthentication
139
+ connection.use Her::Middleware::DefaultParseJSON
140
+ connection.use Faraday::Adapter::NetHttp
141
+ end
142
+ ```
143
+
144
+ Now, each HTTP request made by Her will have the `X-API-Token` header.
145
+
146
+ ### Parsing JSON data
147
+
148
+ By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
149
+
150
+ ```javascript
151
+ // The response of GET /users/1
152
+ { "id" : 1, "name" : "Tobias Fünke" }
153
+
154
+ // The response of GET /users
155
+ [{ "id" : 1, "name" : "Tobias Fünke" }]
156
+ ```
157
+
158
+ 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:
159
+
160
+ ```ruby
161
+ # Expects responses like:
162
+ #
163
+ # {
164
+ # "result": {
165
+ # "id": 1,
166
+ # "name": "Tobias Fünke"
167
+ # },
168
+ # "errors" => []
169
+ # }
170
+ #
171
+ class MyCustomParser < Faraday::Response::Middleware
172
+ def on_complete(env)
173
+ json = MultiJson.load(env[:body], :symbolize_keys => true)
174
+ env[:body] = {
175
+ :data => json[:result],
176
+ :errors => json[:errors],
177
+ :metadata => json[:metadata]
178
+ }
179
+ end
180
+ end
181
+
182
+ Her::API.setup :url => "https://api.example.com" do |connection|
183
+ connection.use MyCustomParser
184
+ connection.use Faraday::Adapter::NetHttp
185
+ end
186
+ ```
187
+
188
+ ### OAuth
189
+
190
+ Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
191
+
192
+ In your Gemfile:
193
+
194
+ ```ruby
195
+ gem "her"
196
+ gem "faraday_middleware"
197
+ gem "simple_oauth"
198
+ ```
199
+
200
+ In your Ruby code:
201
+
202
+ ```ruby
203
+ # Create an application on `https://dev.twitter.com/apps` to set these values
204
+ TWITTER_CREDENTIALS = {
205
+ :consumer_key => "",
206
+ :consumer_secret => "",
207
+ :token => "",
208
+ :token_secret => ""
209
+ }
210
+
211
+ Her::API.setup :url => "https://api.twitter.com/1/" do |connection|
212
+ connection.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
213
+ connection.use Her::Middleware::DefaultParseJSON
214
+ connection.use Faraday::Adapter::NetHttp
215
+ end
216
+
217
+ class Tweet
218
+ include Her::Model
219
+ end
220
+
221
+ @tweets = Tweet.get("/statuses/home_timeline.json")
222
+ ```
223
+
224
+ See the *Authentication* middleware section for an example of how to pass different credentials based on the current user.
225
+
226
+ ### Caching
70
227
 
71
- ## Features
228
+ Again, using the `faraday_middleware` and `memcached` gems makes it very easy to cache requests and responses.
229
+
230
+ In your Gemfile:
231
+
232
+ ```ruby
233
+ gem "her"
234
+ gem "faraday_middleware"
235
+ gem "memcached"
236
+ ```
237
+
238
+ In your Ruby code:
239
+
240
+ ```ruby
241
+ Her::API.setup :url => "https://api.example.com" do |connection|
242
+ connection.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
243
+ connection.use Her::Middleware::DefaultParseJSON
244
+ connection.use Faraday::Adapter::NetHttp
245
+ end
72
246
 
73
- See [FEATURES.md](https://github.com/remiprev/her/blob/master/FEATURES.md) to learn about Her’s advanced features.
247
+ class User
248
+ include Her::Model
249
+ end
250
+
251
+ @user = User.find(1)
252
+ # GET /users/1
253
+
254
+ @user = User.find(1)
255
+ # This request will be fetched from memcached
256
+ ```
257
+
258
+ ## Advanced Features
259
+
260
+ Here’s a list of several useful features available in Her.
261
+
262
+ ### Relationships
263
+
264
+ You can define `has_many`, `has_one` and `belongs_to` relationships in your models. The relationship data is handled in two different ways.
265
+
266
+ 1. If Her finds relationship data when parsing a resource, that data will be used to create the associated model objects on the resource.
267
+ 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).
268
+
269
+ For example:
270
+
271
+ ```ruby
272
+ class User
273
+ include Her::Model
274
+ has_many :comments
275
+ has_one :role
276
+ belongs_to :organization
277
+ end
278
+
279
+ class Comment
280
+ include Her::Model
281
+ end
282
+
283
+ class Role
284
+ include Her::Model
285
+ end
286
+
287
+ class Organization
288
+ include Her::Model
289
+ end
290
+ ```
291
+
292
+ 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:
293
+
294
+ ```ruby
295
+ @user = User.find(1)
296
+ # {
297
+ # :data => {
298
+ # :id => 1,
299
+ # :name => "George Michael Bluth",
300
+ # :comments => [
301
+ # { :id => 1, :text => "Foo" },
302
+ # { :id => 2, :text => "Bar" }
303
+ # ],
304
+ # :role => { :id => 1, :name => "Admin" },
305
+ # :organization => { :id => 2, :name => "Bluth Company" }
306
+ # }
307
+ # }
308
+ @user.comments
309
+ # [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
310
+ @user.role
311
+ # #<Role id=1 name="Admin">
312
+ @user.organization
313
+ # #<Organization id=2 name="Bluth Company">
314
+ ```
315
+
316
+ If there’s no relationship data in the resource, Her makes a HTTP request to retrieve the data.
317
+
318
+ ```ruby
319
+ @user = User.find(1)
320
+ # { :data => { :id => 1, :name => "George Michael Bluth", :organization_id => 2 }}
321
+
322
+ # has_many relationship:
323
+ @user.comments
324
+ # GET /users/1/comments
325
+ # [#<Comment id=1>, #<Comment id=2>]
326
+
327
+ # has_one relationship:
328
+ @user.role
329
+ # GET /users/1/role
330
+ # #<Role id=1>
331
+
332
+ # belongs_to relationship:
333
+ @user.organization
334
+ # (the organization id comes from :organization_id, by default)
335
+ # GET /organizations/2
336
+ # #<Organization id=2>
337
+ ```
338
+
339
+ Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
340
+
341
+ ### Hooks (callbacks)
342
+
343
+ You can add *before* and *after* hooks to your models that are triggered on specific actions. You can use symbols or blocks.
344
+
345
+ ```ruby
346
+ class User
347
+ include Her::Model
348
+ before_save :set_internal_id
349
+ after_find { |u| u.fullname.upcase! }
350
+
351
+ def set_internal_id
352
+ self.internal_id = 42 # Will be passed in the HTTP request
353
+ end
354
+ end
355
+
356
+ @user = User.create(:fullname => "Tobias Funke")
357
+ # POST /users&fullname=Tobias+Fünke&internal_id=42
358
+
359
+ @user = User.find(1)
360
+ @user.fullname # => "TOBIAS FUNKE"
361
+ ```
362
+
363
+ The available hooks are:
364
+
365
+ * `before_save`
366
+ * `before_create`
367
+ * `before_update`
368
+ * `before_destroy`
369
+ * `after_save`
370
+ * `after_create`
371
+ * `after_update`
372
+ * `after_destroy`
373
+ * `after_find`
374
+
375
+ ### Custom requests
376
+
377
+ You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
378
+
379
+ ```ruby
380
+ class User
381
+ include Her::Model
382
+ custom_get :popular, :unpopular
383
+ custom_post :from_default
384
+ end
385
+
386
+ User.popular
387
+ # GET /users/popular
388
+ # [#<User id=1>, #<User id=2>]
389
+
390
+ User.unpopular
391
+ # GET /users/unpopular
392
+ # [#<User id=3>, #<User id=4>]
393
+
394
+ User.from_default(:name => "Maeby Fünke")
395
+ # POST /users/from_default?name=Maeby+Fünke
396
+ # #<User id=5 name="Maeby Fünke">
397
+ ```
398
+
399
+ You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
400
+
401
+ ```ruby
402
+ class User
403
+ include Her::Model
404
+ end
405
+
406
+ User.get(:popular)
407
+ # GET /users/popular
408
+ # [#<User id=1>, #<User id=2>]
409
+
410
+ User.get(:single_best)
411
+ # GET /users/single_best
412
+ # #<User id=1>
413
+ ```
414
+
415
+ 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.).
416
+
417
+ ```ruby
418
+ class User
419
+ include Her::Model
420
+
421
+ def self.popular
422
+ get_collection(:popular)
423
+ end
424
+
425
+ def self.total
426
+ get_raw(:stats) do |parsed_data|
427
+ parsed_data[:data][:total_users]
428
+ end
429
+ end
430
+ end
431
+
432
+ User.popular
433
+ # GET /users/popular
434
+ # [#<User id=1>, #<User id=2>]
435
+ User.total
436
+ # GET /users/stats
437
+ # => 42
438
+ ```
439
+
440
+ You can also use full request paths (with strings instead of symbols).
441
+
442
+ ```ruby
443
+ class User
444
+ include Her::Model
445
+ end
446
+
447
+ User.get("/users/popular")
448
+ # GET /users/popular
449
+ # [#<User id=1>, #<User id=2>]
450
+ ```
451
+
452
+ ### Custom paths
453
+
454
+ You can define custom HTTP paths for your models:
455
+
456
+ ```ruby
457
+ class User
458
+ include Her::Model
459
+ collection_path "/hello_users/:id"
460
+ end
461
+
462
+ @user = User.find(1)
463
+ # GET /hello_users/1
464
+ ```
465
+
466
+ You can also include custom variables in your paths:
467
+
468
+ ```ruby
469
+ class User
470
+ include Her::Model
471
+ collection_path "/organizations/:organization_id/users"
472
+ end
473
+
474
+ @user = User.find(1, :_organization_id => 2)
475
+ # GET /organizations/2/users/1
476
+
477
+ @user = User.all(:_organization_id => 2)
478
+ # GET /organizations/2/users
479
+
480
+ @user = User.new(:fullname => "Tobias Fünke", :organization_id => 2)
481
+ @user.save
482
+ # POST /organizations/2/users
483
+ ```
484
+
485
+ ### Multiple APIs
486
+
487
+ It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
488
+
489
+ ```ruby
490
+ # config/initializers/her.rb
491
+ $my_api = Her::API.new
492
+ $my_api.setup :url => "https://my_api.example.com" do |connection|
493
+ connection.use Her::Middleware::DefaultParseJSON
494
+ connection.use Faraday::Adapter::NetHttp
495
+ end
496
+
497
+ $other_api = Her::API.new
498
+ $other_api.setup :url => "https://other_api.example.com" do |connection|
499
+ connection.use Her::Middleware::DefaultParseJSON
500
+ connection.use Faraday::Adapter::NetHttp
501
+ end
502
+ ```
503
+
504
+ You can then define which API a model will use:
505
+
506
+ ```ruby
507
+ class User
508
+ include Her::Model
509
+ uses_api $my_api
510
+ end
511
+
512
+ class Category
513
+ include Her::Model
514
+ uses_api $other_api
515
+ end
516
+
517
+ User.all
518
+ # GET https://my_api.example.com/users
519
+
520
+ Category.all
521
+ # GET https://other_api.example.com/categories
522
+ ```
523
+
524
+ ### SSL
525
+
526
+ 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.
527
+
528
+ ```ruby
529
+ ssl_options = { :ca_path => "/usr/lib/ssl/certs" }
530
+ Her::API.setup :url => "https://api.example.com", :ssl => ssl_options do |connection|
531
+ connection.use Her::Middleware::DefaultParseJSON
532
+ connection.use Faraday::Adapter::NetHttp
533
+ end
534
+ ```
74
535
 
75
536
  ## Testing
76
537
 
77
- See [TESTING.md](https://github.com/remiprev/her/blob/master/TESTING.md) to learn how to test models using stubbed HTTP requests.
538
+ Suppose we have these two models bound to your API:
539
+
540
+ ```ruby
541
+ # app/models/user.rb
542
+ class User
543
+ include Her::Model
544
+ custom_get :popular
545
+ end
546
+
547
+ # app/models/post.rb
548
+ class Post
549
+ include Her::Model
550
+ custom_get :recent, :archived
551
+ end
552
+ ```
553
+
554
+ In order to test them, we’ll have to stub the remote API requests. With [RSpec](https://github.com/rspec/rspec-core), we can do this like so:
555
+
556
+ ```ruby
557
+ # spec/spec_helper.rb
558
+ RSpec.configure do |config|
559
+ config.include(Module.new do
560
+ def stub_api_for(klass)
561
+ klass.uses_api (api = Her::API.new)
562
+
563
+ # Here, you would customize this for your own API (URL, middleware, etc)
564
+ # like you have done in your application’s initializer
565
+ api.setup :url => "http://api.example.com" do |connection|
566
+ connection.use Her::Middleware::FirstLevelParseJSON
567
+ connection.adapter(:test) { |s| yield(s) }
568
+ end
569
+ end
570
+ end)
571
+ end
572
+ ```
573
+
574
+ Then, in your tests, we can specify what (fake) HTTP requests will return:
575
+
576
+ ```ruby
577
+ # spec/models/user.rb
578
+ describe User do
579
+ before do
580
+ stub_api_for(User) do |stub|
581
+ stub.get("/users/popular") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] }
582
+ end
583
+ end
584
+
585
+ describe :popular do
586
+ subject { User.popular }
587
+ its(:length) { should == 2 }
588
+ its(:errors) { should be_empty }
589
+ end
590
+ end
591
+ ```
592
+
593
+ We can redefine the API for a model as many times as we want, like for more complex tests:
594
+
595
+ ```ruby
596
+ # spec/models/user.rb
597
+ describe Post do
598
+ describe :recent do
599
+ before do
600
+ stub_api_for(Post) do |stub|
601
+ stub.get("/posts/recent") { |env| [200, {}, [{ :id => 1 }, { :id => 2 }].to_json] }
602
+ end
603
+ end
604
+
605
+ subject { Post.recent }
606
+ its(:length) { should == 2 }
607
+ its(:errors) { should be_empty }
608
+ end
609
+
610
+ describe :archived do
611
+ before do
612
+ stub_api_for(Post) do |stub|
613
+ stub.get("/posts/archived") { |env| [200, {}, [{ :id => 1 }, { :id => 2 }].to_json] }
614
+ end
615
+ end
616
+
617
+ subject { Post.archived }
618
+ its(:length) { should == 2 }
619
+ its(:errors) { should be_empty }
620
+ end
621
+ end
622
+ ```
78
623
 
79
624
  ## Upgrade
80
625
 
81
- See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for backward compability issues.
626
+ See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/docs/UPGRADE.md) for backward compability issues.
82
627
 
83
628
  ## Her IRL
84
629
 
85
630
  Most projects I know that use Her are internal or private projects but here’s a list of public ones:
86
631
 
87
632
  * [tumbz](https://github.com/remiprev/tumbz)
633
+ * [crowdher](https://github.com/simonprev/crowdher)
634
+
635
+ ## History
636
+
637
+ 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.
638
+
639
+ 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).
88
640
 
89
641
  ## Contribute
90
642
 
91
643
  Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues).
92
644
 
93
- See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices.
645
+ See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/docs/CONTRIBUTING.md) for best practices.
94
646
 
95
647
  ### Contributors
96
648
 
@@ -107,6 +659,9 @@ These fine folks helped with Her:
107
659
  * [@calmyournerves](https://github.com/calmyournerves)
108
660
  * [@luflux](https://github.com/luxflux)
109
661
  * [@simonc](https://github.com/simonc)
662
+ * [@pencil](https://github.com/pencil)
663
+ * [@joanniclaborde](https://github.com/joanniclaborde)
664
+ * [@seanreads](https://github.com/seanreads)
110
665
 
111
666
  ## License
112
667