extended_her 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (66) hide show
  1. data/.gitignore +8 -0
  2. data/.rspec +2 -0
  3. data/.travis.yml +8 -0
  4. data/CONTRIBUTING.md +26 -0
  5. data/Gemfile +2 -0
  6. data/LICENSE +7 -0
  7. data/README.md +723 -0
  8. data/Rakefile +11 -0
  9. data/UPGRADE.md +32 -0
  10. data/examples/twitter-oauth/Gemfile +13 -0
  11. data/examples/twitter-oauth/app.rb +50 -0
  12. data/examples/twitter-oauth/config.ru +5 -0
  13. data/examples/twitter-oauth/views/index.haml +9 -0
  14. data/examples/twitter-search/Gemfile +12 -0
  15. data/examples/twitter-search/app.rb +55 -0
  16. data/examples/twitter-search/config.ru +5 -0
  17. data/examples/twitter-search/views/index.haml +9 -0
  18. data/extended_her.gemspec +27 -0
  19. data/lib/her.rb +23 -0
  20. data/lib/her/api.rb +108 -0
  21. data/lib/her/base.rb +17 -0
  22. data/lib/her/collection.rb +12 -0
  23. data/lib/her/errors.rb +5 -0
  24. data/lib/her/exceptions/exception.rb +4 -0
  25. data/lib/her/exceptions/record_invalid.rb +8 -0
  26. data/lib/her/exceptions/record_not_found.rb +13 -0
  27. data/lib/her/middleware.rb +9 -0
  28. data/lib/her/middleware/accept_json.rb +15 -0
  29. data/lib/her/middleware/first_level_parse_json.rb +34 -0
  30. data/lib/her/middleware/second_level_parse_json.rb +28 -0
  31. data/lib/her/model.rb +69 -0
  32. data/lib/her/model/base.rb +7 -0
  33. data/lib/her/model/hooks.rb +114 -0
  34. data/lib/her/model/http.rb +284 -0
  35. data/lib/her/model/introspection.rb +57 -0
  36. data/lib/her/model/orm.rb +191 -0
  37. data/lib/her/model/orm/comparison_methods.rb +20 -0
  38. data/lib/her/model/orm/create_methods.rb +29 -0
  39. data/lib/her/model/orm/destroy_methods.rb +53 -0
  40. data/lib/her/model/orm/error_methods.rb +19 -0
  41. data/lib/her/model/orm/fields_definition.rb +15 -0
  42. data/lib/her/model/orm/find_methods.rb +46 -0
  43. data/lib/her/model/orm/persistance_methods.rb +22 -0
  44. data/lib/her/model/orm/relation_mapper.rb +21 -0
  45. data/lib/her/model/orm/save_methods.rb +58 -0
  46. data/lib/her/model/orm/serialization_methods.rb +28 -0
  47. data/lib/her/model/orm/update_methods.rb +31 -0
  48. data/lib/her/model/paths.rb +82 -0
  49. data/lib/her/model/relationships.rb +191 -0
  50. data/lib/her/paginated_collection.rb +20 -0
  51. data/lib/her/relation.rb +94 -0
  52. data/lib/her/version.rb +3 -0
  53. data/spec/api_spec.rb +131 -0
  54. data/spec/collection_spec.rb +26 -0
  55. data/spec/middleware/accept_json_spec.rb +10 -0
  56. data/spec/middleware/first_level_parse_json_spec.rb +42 -0
  57. data/spec/middleware/second_level_parse_json_spec.rb +25 -0
  58. data/spec/model/hooks_spec.rb +406 -0
  59. data/spec/model/http_spec.rb +184 -0
  60. data/spec/model/introspection_spec.rb +59 -0
  61. data/spec/model/orm_spec.rb +552 -0
  62. data/spec/model/paths_spec.rb +286 -0
  63. data/spec/model/relationships_spec.rb +222 -0
  64. data/spec/model_spec.rb +31 -0
  65. data/spec/spec_helper.rb +46 -0
  66. metadata +222 -0
@@ -0,0 +1,8 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
5
+ .yardoc
6
+ doc
7
+ rake
8
+ tmp
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --color
2
+ --format=documentation
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 1.9.3
5
+ - 1.9.2
6
+ - 1.8.7
7
+
8
+ script: "bundle exec rake spec"
@@ -0,0 +1,26 @@
1
+ # How to contribute
2
+
3
+ _(This file is heavily based on [factory\_girl\_rails](https://github.com/thoughtbot/factory_girl_rails/blob/master/CONTRIBUTING.md)’s Contribution Guide)_
4
+
5
+ We love pull requests. Here’s a quick guide:
6
+
7
+ * Fork the repository.
8
+ * Run `rake spec` (to make sure you start with a clean slate).
9
+ * Implement your feature or fix.
10
+ * Add examples that describe it (in the `spec` directory). Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, we need examples!
11
+ * Make sure `rake spec` passes after your modifications.
12
+ * Commit (bonus points for doing it in a `feature-*` branch).
13
+ * Push to your fork and send your pull request!
14
+
15
+ If we have not replied to your pull request in three or four days, do not hesitate to post another comment in it — yes, we can be lazy sometimes.
16
+
17
+ ## Syntax Guide
18
+
19
+ Do not hesitate to submit patches that fix syntax issues. Some may have slipped under our nose.
20
+
21
+ * Two spaces, no tabs (but you already knew that, right?).
22
+ * No trailing whitespace. Blank lines should not have any space. There are few things we **hate** more than trailing whitespace. Seriously.
23
+ * `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
24
+ * `[:foo, :bar]` and not `[ :foo, :bar ]`, `{ :foo => :bar }` and not `{:foo => :bar}`
25
+ * `a = b` and not `a=b`.
26
+ * Follow the conventions you see used in the source already.
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright (c) 2012 Rémi Prévost
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,723 @@
1
+ # Her [![Build Status](https://secure.travis-ci.org/remiprev/her.png?branch=master)](http://travis-ci.org/remiprev/her) [![Gem dependency status](https://gemnasium.com/remiprev/her.png?travis)](https://gemnasium.com/remiprev/her)
2
+
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.
4
+
5
+ ## Installation
6
+
7
+ In your Gemfile, add:
8
+
9
+ ```ruby
10
+ gem "her"
11
+ ```
12
+
13
+ That’s it!
14
+
15
+ ## Usage
16
+
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:
18
+
19
+ ```ruby
20
+ # config/initializers/her.rb
21
+ Her::API.setup :url => "https://api.example.com" do |connection|
22
+ connection.use Faraday::Request::UrlEncoded
23
+ connection.use Her::Middleware::DefaultParseJSON
24
+ connection.use Faraday::Adapter::NetHttp
25
+ end
26
+ ```
27
+
28
+ And then to add the ORM behavior to a class, you just have to include `Her::Model` in it:
29
+
30
+ ```ruby
31
+ class User
32
+ include Her::Model
33
+ end
34
+ ```
35
+
36
+ After that, using Her is very similar to many ActiveRecord-like ORMs:
37
+
38
+ ```ruby
39
+ User.all
40
+ # GET https://api.example.com/users and return an array of User objects
41
+
42
+ User.find(1)
43
+ # GET https://api.example.com/users/1 and return a User object
44
+
45
+ @user = User.create(:fullname => "Tobias Fünke")
46
+ # POST "https://api.example.com/users" with the data and return a User object
47
+
48
+ @user = User.new(:fullname => "Tobias Fünke")
49
+ @user.occupation = "actor"
50
+ @user.save
51
+ # POST https://api.example.com/users with the data and return a User object
52
+
53
+ @user = User.find(1)
54
+ @user.fullname = "Lindsay Fünke"
55
+ @user.save
56
+ # PUT https://api.example.com/users/1 with the data and return+update the User object
57
+ ```
58
+
59
+ ### ActiveRecord-like methods
60
+
61
+ These are the basic ActiveRecord-like methods you can use with your models:
62
+
63
+ ```ruby
64
+ class User
65
+ include Her::Model
66
+ end
67
+
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).
96
+
97
+ ## Middleware
98
+
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
+ ### OAuth
147
+
148
+ Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
149
+
150
+ In your Gemfile:
151
+
152
+ ```ruby
153
+ gem "her"
154
+ gem "faraday_middleware"
155
+ gem "simple_oauth"
156
+ ```
157
+
158
+ In your Ruby code:
159
+
160
+ ```ruby
161
+ # Create an application on `https://dev.twitter.com/apps` to set these values
162
+ TWITTER_CREDENTIALS = {
163
+ :consumer_key => "",
164
+ :consumer_secret => "",
165
+ :token => "",
166
+ :token_secret => ""
167
+ }
168
+
169
+ Her::API.setup :url => "https://api.twitter.com/1/" do |connection|
170
+ connection.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
171
+ connection.use Her::Middleware::DefaultParseJSON
172
+ connection.use Faraday::Adapter::NetHttp
173
+ end
174
+
175
+ class Tweet
176
+ include Her::Model
177
+ end
178
+
179
+ @tweets = Tweet.get("/statuses/home_timeline.json")
180
+ ```
181
+
182
+ See the *Authentication* middleware section for an example of how to pass different credentials based on the current user.
183
+
184
+ ### Parsing JSON data
185
+
186
+ By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
187
+
188
+ ```javascript
189
+ // The response of GET /users/1
190
+ { "id" : 1, "name" : "Tobias Fünke" }
191
+
192
+ // The response of GET /users
193
+ [{ "id" : 1, "name" : "Tobias Fünke" }]
194
+ ```
195
+
196
+ However, if you want Her to be able to parse the data from a single root element (usually based on the model name), you’ll have to use the `parse_root_in_json` method (See the **JSON attributes-wrapping** section).
197
+
198
+ Also, 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:
199
+
200
+ ```ruby
201
+ # Expects responses like:
202
+ #
203
+ # {
204
+ # "result": {
205
+ # "id": 1,
206
+ # "name": "Tobias Fünke"
207
+ # },
208
+ # "errors" => []
209
+ # }
210
+ #
211
+ class MyCustomParser < Faraday::Response::Middleware
212
+ def on_complete(env)
213
+ json = MultiJson.load(env[:body], :symbolize_keys => true)
214
+ env[:body] = {
215
+ :data => json[:result],
216
+ :errors => json[:errors],
217
+ :metadata => json[:metadata]
218
+ }
219
+ end
220
+ end
221
+
222
+ Her::API.setup :url => "https://api.example.com" do |connection|
223
+ connection.use MyCustomParser
224
+ connection.use Faraday::Adapter::NetHttp
225
+ end
226
+ ```
227
+
228
+ ### Caching
229
+
230
+ Again, using the `faraday_middleware` and `memcached` gems makes it very easy to cache requests and responses.
231
+
232
+ In your Gemfile:
233
+
234
+ ```ruby
235
+ gem "her"
236
+ gem "faraday_middleware"
237
+ gem "memcached"
238
+ ```
239
+
240
+ In your Ruby code:
241
+
242
+ ```ruby
243
+ Her::API.setup :url => "https://api.example.com" do |connection|
244
+ connection.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
245
+ connection.use Her::Middleware::DefaultParseJSON
246
+ connection.use Faraday::Adapter::NetHttp
247
+ end
248
+
249
+ class User
250
+ include Her::Model
251
+ end
252
+
253
+ @user = User.find(1)
254
+ # GET /users/1
255
+
256
+ @user = User.find(1)
257
+ # This request will be fetched from memcached
258
+ ```
259
+
260
+ ## Advanced Features
261
+
262
+ Here’s a list of several useful features available in Her.
263
+
264
+ ### Relationships
265
+
266
+ You can define `has_many`, `has_one` and `belongs_to` relationships in your models. The relationship data is handled in two different ways.
267
+
268
+ 1. If Her finds relationship data when parsing a resource, that data will be used to create the associated model objects on the resource.
269
+ 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).
270
+
271
+ For example:
272
+
273
+ ```ruby
274
+ class User
275
+ include Her::Model
276
+ has_many :comments
277
+ has_one :role
278
+ belongs_to :organization
279
+ end
280
+
281
+ class Comment
282
+ include Her::Model
283
+ end
284
+
285
+ class Role
286
+ include Her::Model
287
+ end
288
+
289
+ class Organization
290
+ include Her::Model
291
+ end
292
+ ```
293
+
294
+ 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:
295
+
296
+ ```ruby
297
+ @user = User.find(1)
298
+ # {
299
+ # :data => {
300
+ # :id => 1,
301
+ # :name => "George Michael Bluth",
302
+ # :comments => [
303
+ # { :id => 1, :text => "Foo" },
304
+ # { :id => 2, :text => "Bar" }
305
+ # ],
306
+ # :role => { :id => 1, :name => "Admin" },
307
+ # :organization => { :id => 2, :name => "Bluth Company" }
308
+ # }
309
+ # }
310
+ @user.comments
311
+ # [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
312
+ @user.role
313
+ # #<Role id=1 name="Admin">
314
+ @user.organization
315
+ # #<Organization id=2 name="Bluth Company">
316
+ ```
317
+
318
+ If there’s no relationship data in the resource, Her makes a HTTP request to retrieve the data.
319
+
320
+ ```ruby
321
+ @user = User.find(1)
322
+ # { :data => { :id => 1, :name => "George Michael Bluth", :organization_id => 2 }}
323
+
324
+ # has_many relationship:
325
+ @user.comments
326
+ # GET /users/1/comments
327
+ # [#<Comment id=1>, #<Comment id=2>]
328
+
329
+ # has_one relationship:
330
+ @user.role
331
+ # GET /users/1/role
332
+ # #<Role id=1>
333
+
334
+ # belongs_to relationship:
335
+ @user.organization
336
+ # (the organization id comes from :organization_id, by default)
337
+ # GET /organizations/2
338
+ # #<Organization id=2>
339
+ ```
340
+
341
+ Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
342
+
343
+ ### Hooks (callbacks)
344
+
345
+ You can add *before* and *after* hooks to your models that are triggered on specific actions. You can use symbols or blocks.
346
+
347
+ ```ruby
348
+ class User
349
+ include Her::Model
350
+ before_save :set_internal_id
351
+ after_find { |u| u.fullname.upcase! }
352
+
353
+ def set_internal_id
354
+ self.internal_id = 42 # Will be passed in the HTTP request
355
+ end
356
+ end
357
+
358
+ @user = User.create(:fullname => "Tobias Funke")
359
+ # POST /users&fullname=Tobias+Fünke&internal_id=42
360
+
361
+ @user = User.find(1)
362
+ @user.fullname # => "TOBIAS FUNKE"
363
+ ```
364
+
365
+ The available hooks are:
366
+
367
+ * `before_save`
368
+ * `before_create`
369
+ * `before_update`
370
+ * `before_destroy`
371
+ * `after_save`
372
+ * `after_create`
373
+ * `after_update`
374
+ * `after_destroy`
375
+ * `after_find`
376
+
377
+ ### JSON attributes-wrapping
378
+
379
+ Her supports *sending* and *parsing* JSON data wrapped in a root element (to be compatible with Rails’ `include_root_in_json` setting), like so:
380
+
381
+ #### Sending
382
+
383
+ If you want to send all data to your API wrapped in a *root* element based on the model name.
384
+
385
+ ```ruby
386
+ class User
387
+ include Her::Model
388
+ include_root_in_json true
389
+ end
390
+
391
+ class Article
392
+ include Her::Model
393
+ include_root_in_json :post
394
+ end
395
+
396
+ User.create(:fullname => "Tobias Fünke")
397
+ # POST { "user": { "fullname": "Tobias Fünke" } } to /users
398
+
399
+ Article.create(:title => "Hello world.")
400
+ # POST { "post": { "title": "Hello world." } } to /articles
401
+ ```
402
+
403
+ #### Parsing
404
+
405
+ If the API returns data wrapped in a *root* element based on the model name.
406
+
407
+ ```ruby
408
+ class User
409
+ include Her::Model
410
+ parse_root_in_json true
411
+ end
412
+
413
+ class Article
414
+ include Her::Model
415
+ parse_root_in_json :post
416
+ end
417
+
418
+ # POST /users returns { "user": { "fullname": "Tobias Fünke" } }
419
+ user = User.create(:fullname => "Tobias Fünke")
420
+ user.fullname # => "Tobias Fünke"
421
+
422
+ # POST /articles returns { "post": { "title": "Hello world." } }
423
+ article = Article.create(:title => "Hello world.")
424
+ article.title # => "Hello world."
425
+ ```
426
+
427
+ Of course, you can use both `include_root_in_json` and `parse_root_in_json` at the same time.
428
+
429
+ ### Custom requests
430
+
431
+ You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
432
+
433
+ ```ruby
434
+ class User
435
+ include Her::Model
436
+ custom_get :popular, :unpopular
437
+ custom_post :from_default
438
+ end
439
+
440
+ User.popular
441
+ # GET /users/popular
442
+ # [#<User id=1>, #<User id=2>]
443
+
444
+ User.unpopular
445
+ # GET /users/unpopular
446
+ # [#<User id=3>, #<User id=4>]
447
+
448
+ User.from_default(:name => "Maeby Fünke")
449
+ # POST /users/from_default?name=Maeby+Fünke
450
+ # #<User id=5 name="Maeby Fünke">
451
+ ```
452
+
453
+ You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
454
+
455
+ ```ruby
456
+ class User
457
+ include Her::Model
458
+ end
459
+
460
+ User.get(:popular)
461
+ # GET /users/popular
462
+ # [#<User id=1>, #<User id=2>]
463
+
464
+ User.get(:single_best)
465
+ # GET /users/single_best
466
+ # #<User id=1>
467
+ ```
468
+
469
+ 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.).
470
+
471
+ ```ruby
472
+ class User
473
+ include Her::Model
474
+
475
+ def self.popular
476
+ get_collection(:popular)
477
+ end
478
+
479
+ def self.total
480
+ get_raw(:stats) do |parsed_data|
481
+ parsed_data[:data][:total_users]
482
+ end
483
+ end
484
+ end
485
+
486
+ User.popular
487
+ # GET /users/popular
488
+ # [#<User id=1>, #<User id=2>]
489
+ User.total
490
+ # GET /users/stats
491
+ # => 42
492
+ ```
493
+
494
+ You can also use full request paths (with strings instead of symbols).
495
+
496
+ ```ruby
497
+ class User
498
+ include Her::Model
499
+ end
500
+
501
+ User.get("/users/popular")
502
+ # GET /users/popular
503
+ # [#<User id=1>, #<User id=2>]
504
+ ```
505
+
506
+ ### Custom paths
507
+
508
+ You can define custom HTTP paths for your models:
509
+
510
+ ```ruby
511
+ class User
512
+ include Her::Model
513
+ collection_path "/hello_users/:id"
514
+ end
515
+
516
+ @user = User.find(1)
517
+ # GET /hello_users/1
518
+ ```
519
+
520
+ You can also include custom variables in your paths:
521
+
522
+ ```ruby
523
+ class User
524
+ include Her::Model
525
+ collection_path "/organizations/:organization_id/users"
526
+ end
527
+
528
+ @user = User.find(1, :_organization_id => 2)
529
+ # GET /organizations/2/users/1
530
+
531
+ @user = User.all(:_organization_id => 2)
532
+ # GET /organizations/2/users
533
+
534
+ @user = User.new(:fullname => "Tobias Fünke", :organization_id => 2)
535
+ @user.save
536
+ # POST /organizations/2/users
537
+ ```
538
+
539
+ ### Multiple APIs
540
+
541
+ It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
542
+
543
+ ```ruby
544
+ # config/initializers/her.rb
545
+ $my_api = Her::API.new
546
+ $my_api.setup :url => "https://my_api.example.com" do |connection|
547
+ connection.use Her::Middleware::DefaultParseJSON
548
+ connection.use Faraday::Adapter::NetHttp
549
+ end
550
+
551
+ $other_api = Her::API.new
552
+ $other_api.setup :url => "https://other_api.example.com" do |connection|
553
+ connection.use Her::Middleware::DefaultParseJSON
554
+ connection.use Faraday::Adapter::NetHttp
555
+ end
556
+ ```
557
+
558
+ You can then define which API a model will use:
559
+
560
+ ```ruby
561
+ class User
562
+ include Her::Model
563
+ uses_api $my_api
564
+ end
565
+
566
+ class Category
567
+ include Her::Model
568
+ uses_api $other_api
569
+ end
570
+
571
+ User.all
572
+ # GET https://my_api.example.com/users
573
+
574
+ Category.all
575
+ # GET https://other_api.example.com/categories
576
+ ```
577
+
578
+ ### SSL
579
+
580
+ 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.
581
+
582
+ ```ruby
583
+ ssl_options = { :ca_path => "/usr/lib/ssl/certs" }
584
+ Her::API.setup :url => "https://api.example.com", :ssl => ssl_options do |connection|
585
+ connection.use Her::Middleware::DefaultParseJSON
586
+ connection.use Faraday::Adapter::NetHttp
587
+ end
588
+ ```
589
+
590
+ ## Testing
591
+
592
+ Suppose we have these two models bound to your API:
593
+
594
+ ```ruby
595
+ # app/models/user.rb
596
+ class User
597
+ include Her::Model
598
+ custom_get :popular
599
+ end
600
+
601
+ # app/models/post.rb
602
+ class Post
603
+ include Her::Model
604
+ custom_get :recent, :archived
605
+ end
606
+ ```
607
+
608
+ 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:
609
+
610
+ ```ruby
611
+ # spec/spec_helper.rb
612
+ RSpec.configure do |config|
613
+ config.include(Module.new do
614
+ def stub_api_for(klass)
615
+ klass.uses_api (api = Her::API.new)
616
+
617
+ # Here, you would customize this for your own API (URL, middleware, etc)
618
+ # like you have done in your application’s initializer
619
+ api.setup :url => "http://api.example.com" do |connection|
620
+ connection.use Her::Middleware::FirstLevelParseJSON
621
+ connection.adapter(:test) { |s| yield(s) }
622
+ end
623
+ end
624
+ end)
625
+ end
626
+ ```
627
+
628
+ Then, in your tests, we can specify what (fake) HTTP requests will return:
629
+
630
+ ```ruby
631
+ # spec/models/user.rb
632
+ describe User do
633
+ before do
634
+ stub_api_for(User) do |stub|
635
+ stub.get("/users/popular") { |env| [200, {}, [{ :id => 1, :name => "Tobias Fünke" }, { :id => 2, :name => "Lindsay Fünke" }].to_json] }
636
+ end
637
+ end
638
+
639
+ describe :popular do
640
+ subject { User.popular }
641
+ its(:length) { should == 2 }
642
+ its(:errors) { should be_empty }
643
+ end
644
+ end
645
+ ```
646
+
647
+ We can redefine the API for a model as many times as we want, like for more complex tests:
648
+
649
+ ```ruby
650
+ # spec/models/user.rb
651
+ describe Post do
652
+ describe :recent do
653
+ before do
654
+ stub_api_for(Post) do |stub|
655
+ stub.get("/posts/recent") { |env| [200, {}, [{ :id => 1 }, { :id => 2 }].to_json] }
656
+ end
657
+ end
658
+
659
+ subject { Post.recent }
660
+ its(:length) { should == 2 }
661
+ its(:errors) { should be_empty }
662
+ end
663
+
664
+ describe :archived do
665
+ before do
666
+ stub_api_for(Post) do |stub|
667
+ stub.get("/posts/archived") { |env| [200, {}, [{ :id => 1 }, { :id => 2 }].to_json] }
668
+ end
669
+ end
670
+
671
+ subject { Post.archived }
672
+ its(:length) { should == 2 }
673
+ its(:errors) { should be_empty }
674
+ end
675
+ end
676
+ ```
677
+
678
+ ## Upgrade
679
+
680
+ See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for backward compability issues.
681
+
682
+ ## Her IRL
683
+
684
+ Most projects I know that use Her are internal or private projects but here’s a list of public ones:
685
+
686
+ * [tumbz](https://github.com/remiprev/tumbz)
687
+ * [crowdher](https://github.com/simonprev/crowdher)
688
+
689
+ ## History
690
+
691
+ 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.
692
+
693
+ 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).
694
+
695
+ ## Contribute
696
+
697
+ Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues).
698
+
699
+ See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices.
700
+
701
+ ### Contributors
702
+
703
+ These fine folks helped with Her:
704
+
705
+ * [@jfcixmedia](https://github.com/jfcixmedia)
706
+ * [@EtienneLem](https://github.com/EtienneLem)
707
+ * [@rafaelss](https://github.com/rafaelss)
708
+ * [@tysontate](https://github.com/tysontate)
709
+ * [@nfo](https://github.com/nfo)
710
+ * [@simonprevost](https://github.com/simonprevost)
711
+ * [@jmlacroix](https://github.com/jmlacroix)
712
+ * [@thomsbg](https://github.com/thomsbg)
713
+ * [@calmyournerves](https://github.com/calmyournerves)
714
+ * [@luflux](https://github.com/luxflux)
715
+ * [@simonc](https://github.com/simonc)
716
+ * [@pencil](https://github.com/pencil)
717
+ * [@joanniclaborde](https://github.com/joanniclaborde)
718
+ * [@seanreads](https://github.com/seanreads)
719
+ * [@jonkarna](https://github.com/jonkarna)
720
+
721
+ ## License
722
+
723
+ 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.