her 0.3.7 → 0.3.8

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