test_track_rails_client 0.9.7 → 0.9.8

Sign up to get free protection for your applications and to get access to all the features.
Files changed (113) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +11 -8
  3. data/lib/test_track_rails_client/version.rb +1 -1
  4. data/vendor/gems/fakeable_her/fakeable_her.gemspec +22 -0
  5. data/vendor/gems/fakeable_her/lib/fakeable_her/model.rb +148 -0
  6. data/vendor/gems/fakeable_her/lib/fakeable_her/version.rb +3 -0
  7. data/vendor/gems/fakeable_her/lib/fakeable_her.rb +5 -0
  8. data/vendor/gems/her/CONTRIBUTING.md +26 -0
  9. data/vendor/gems/her/Gemfile +10 -0
  10. data/vendor/gems/her/LICENSE +7 -0
  11. data/vendor/gems/her/README.md +1023 -0
  12. data/vendor/gems/her/Rakefile +11 -0
  13. data/vendor/gems/her/UPGRADE.md +101 -0
  14. data/vendor/gems/her/gemfiles/Gemfile.activemodel-3.2.x +7 -0
  15. data/vendor/gems/her/gemfiles/Gemfile.activemodel-4.0 +7 -0
  16. data/vendor/gems/her/gemfiles/Gemfile.activemodel-4.1 +7 -0
  17. data/vendor/gems/her/gemfiles/Gemfile.activemodel-4.2 +7 -0
  18. data/vendor/gems/her/her.gemspec +31 -0
  19. data/vendor/gems/her/lib/her/api.rb +119 -0
  20. data/vendor/gems/her/lib/her/collection.rb +12 -0
  21. data/vendor/gems/her/lib/her/error_collection.rb +15 -0
  22. data/vendor/gems/her/lib/her/errors.rb +40 -0
  23. data/vendor/gems/her/lib/her/json_api/model.rb +46 -0
  24. data/vendor/gems/her/lib/her/middleware/accept_json.rb +17 -0
  25. data/vendor/gems/her/lib/her/middleware/first_level_parse_json.rb +36 -0
  26. data/vendor/gems/her/lib/her/middleware/json_api_parser.rb +36 -0
  27. data/vendor/gems/her/lib/her/middleware/parse_json.rb +21 -0
  28. data/vendor/gems/her/lib/her/middleware/second_level_parse_json.rb +36 -0
  29. data/vendor/gems/her/lib/her/middleware.rb +12 -0
  30. data/vendor/gems/her/lib/her/model/active_model_overrides.rb +13 -0
  31. data/vendor/gems/her/lib/her/model/associations/association.rb +106 -0
  32. data/vendor/gems/her/lib/her/model/associations/association_proxy.rb +46 -0
  33. data/vendor/gems/her/lib/her/model/associations/belongs_to_association.rb +96 -0
  34. data/vendor/gems/her/lib/her/model/associations/has_many_association.rb +100 -0
  35. data/vendor/gems/her/lib/her/model/associations/has_one_association.rb +79 -0
  36. data/vendor/gems/her/lib/her/model/associations.rb +141 -0
  37. data/vendor/gems/her/lib/her/model/attributes.rb +304 -0
  38. data/vendor/gems/her/lib/her/model/base.rb +33 -0
  39. data/vendor/gems/her/lib/her/model/deprecated_methods.rb +61 -0
  40. data/vendor/gems/her/lib/her/model/http.rb +117 -0
  41. data/vendor/gems/her/lib/her/model/introspection.rb +65 -0
  42. data/vendor/gems/her/lib/her/model/nested_attributes.rb +45 -0
  43. data/vendor/gems/her/lib/her/model/orm.rb +219 -0
  44. data/vendor/gems/her/lib/her/model/parse.rb +215 -0
  45. data/vendor/gems/her/lib/her/model/paths.rb +126 -0
  46. data/vendor/gems/her/lib/her/model/relation.rb +251 -0
  47. data/vendor/gems/her/lib/her/model.rb +81 -0
  48. data/vendor/gems/her/lib/her/version.rb +3 -0
  49. data/vendor/gems/her/lib/her.rb +20 -0
  50. data/vendor/gems/her/spec/api_spec.rb +114 -0
  51. data/vendor/gems/her/spec/collection_spec.rb +26 -0
  52. data/vendor/gems/her/spec/error_collection_spec.rb +33 -0
  53. data/vendor/gems/her/spec/json_api/model_spec.rb +168 -0
  54. data/vendor/gems/her/spec/middleware/accept_json_spec.rb +10 -0
  55. data/vendor/gems/her/spec/middleware/first_level_parse_json_spec.rb +62 -0
  56. data/vendor/gems/her/spec/middleware/json_api_parser_spec.rb +32 -0
  57. data/vendor/gems/her/spec/middleware/second_level_parse_json_spec.rb +35 -0
  58. data/vendor/gems/her/spec/model/associations/association_proxy_spec.rb +31 -0
  59. data/vendor/gems/her/spec/model/associations_spec.rb +504 -0
  60. data/vendor/gems/her/spec/model/attributes_spec.rb +404 -0
  61. data/vendor/gems/her/spec/model/callbacks_spec.rb +145 -0
  62. data/vendor/gems/her/spec/model/dirty_spec.rb +110 -0
  63. data/vendor/gems/her/spec/model/http_spec.rb +165 -0
  64. data/vendor/gems/her/spec/model/introspection_spec.rb +76 -0
  65. data/vendor/gems/her/spec/model/nested_attributes_spec.rb +134 -0
  66. data/vendor/gems/her/spec/model/orm_spec.rb +791 -0
  67. data/vendor/gems/her/spec/model/parse_spec.rb +372 -0
  68. data/vendor/gems/her/spec/model/paths_spec.rb +347 -0
  69. data/vendor/gems/her/spec/model/relation_spec.rb +226 -0
  70. data/vendor/gems/her/spec/model/validations_spec.rb +42 -0
  71. data/vendor/gems/her/spec/model_spec.rb +31 -0
  72. data/vendor/gems/her/spec/spec_helper.rb +27 -0
  73. data/vendor/gems/her/spec/support/extensions/array.rb +5 -0
  74. data/vendor/gems/her/spec/support/extensions/hash.rb +5 -0
  75. data/vendor/gems/her/spec/support/macros/her_macros.rb +17 -0
  76. data/vendor/gems/her/spec/support/macros/model_macros.rb +36 -0
  77. data/vendor/gems/her/spec/support/macros/request_macros.rb +27 -0
  78. data/vendor/gems/publicsuffix-ruby/CHANGELOG.md +236 -0
  79. data/vendor/gems/publicsuffix-ruby/Gemfile +3 -0
  80. data/vendor/gems/publicsuffix-ruby/LICENSE.txt +22 -0
  81. data/vendor/gems/publicsuffix-ruby/README.md +151 -0
  82. data/vendor/gems/publicsuffix-ruby/Rakefile +109 -0
  83. data/vendor/gems/publicsuffix-ruby/lib/definitions.txt +11467 -0
  84. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/domain.rb +387 -0
  85. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/errors.rb +53 -0
  86. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/list.rb +302 -0
  87. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/rule.rb +373 -0
  88. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/version.rb +23 -0
  89. data/vendor/gems/publicsuffix-ruby/lib/public_suffix.rb +131 -0
  90. data/vendor/gems/publicsuffix-ruby/public_suffix.gemspec +39 -0
  91. data/vendor/gems/publicsuffix-ruby/test/acceptance_test.rb +42 -0
  92. data/vendor/gems/publicsuffix-ruby/test/test_helper.rb +6 -0
  93. data/vendor/gems/publicsuffix-ruby/test/unit/domain_test.rb +170 -0
  94. data/vendor/gems/publicsuffix-ruby/test/unit/errors_test.rb +23 -0
  95. data/vendor/gems/publicsuffix-ruby/test/unit/list_test.rb +179 -0
  96. data/vendor/gems/publicsuffix-ruby/test/unit/public_suffix_test.rb +115 -0
  97. data/vendor/gems/publicsuffix-ruby/test/unit/rule_test.rb +307 -0
  98. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/capybara_configuration.rb +98 -0
  99. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/matchers.rb +151 -0
  100. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rspec_configuration.rb +34 -0
  101. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop/cop/betterment/html_safe.rb +15 -0
  102. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop/cop/betterment/raw.rb +15 -0
  103. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop/cop/betterment/safe_concat.rb +15 -0
  104. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop.rb +3 -0
  105. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/shared_examples/betterment_application_examples.rb +47 -0
  106. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/shared_examples.rb +1 -0
  107. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/site_prism_configuration.rb +42 -0
  108. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/site_prism_dropdown.rb +17 -0
  109. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/version.rb +3 -0
  110. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/webmock_configuration.rb +8 -0
  111. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers.rb +2 -0
  112. data/vendor/gems/ruby_spec_helpers/ruby_spec_helpers.gemspec +25 -0
  113. metadata +110 -1
@@ -0,0 +1,1023 @@
1
+ <p align="center">
2
+ <a href="https://github.com/remiprev/her">
3
+ <img src="http://i.imgur.com/43KEchq.png" alt="Her" />
4
+ </a>
5
+ <br />
6
+ Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects.<br /> It is designed to build applications that are powered by a RESTful API instead of a database.
7
+ <br /><br />
8
+ <a href="https://rubygems.org/gems/her"><img src="http://img.shields.io/gem/v/her.svg" /></a>
9
+ <a href="https://codeclimate.com/github/remiprev/her"><img src="http://img.shields.io/codeclimate/github/remiprev/her.svg" /></a>
10
+ <a href='https://gemnasium.com/remiprev/her'><img src="http://img.shields.io/gemnasium/remiprev/her.svg" /></a>
11
+ <a href="https://travis-ci.com/Betterment/her"><img src="https://travis-ci.com/Betterment/her.svg?token=6b6DErRMUHX47kEoBZ3t&branch=master" /></a>
12
+ </p>
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ In your Gemfile, add:
19
+
20
+ ```ruby
21
+ gem "her"
22
+ ```
23
+
24
+ That’s it!
25
+
26
+ ## Betterment Changes
27
+ ### `Model#request_headers`, `Model#request_options`, `Relation.with_request_headers`, `Relation.with_request_options`
28
+
29
+ You can modify the request headers and timeout of any request using a model's `request_headers` or `request_options` methods. See [the spec](https://github.com/Betterment/her/blob/master/spec/model/orm_spec.rb#L580) for example usage
30
+
31
+
32
+ ## Usage
33
+
34
+ _For a complete reference of all the methods you can use, check out [the documentation](http://rdoc.info/github/remiprev/her)._
35
+
36
+ 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
+
38
+ ```ruby
39
+ # config/initializers/her.rb
40
+ Her::API.setup url: "https://api.example.com" do |c|
41
+ # Request
42
+ c.use Faraday::Request::UrlEncoded
43
+
44
+ # Response
45
+ c.use Her::Middleware::DefaultParseJSON
46
+
47
+ # Adapter
48
+ c.use Faraday::Adapter::NetHttp
49
+ end
50
+ ```
51
+
52
+ And then to add the ORM behavior to a class, you just have to include `Her::Model` in it:
53
+
54
+ ```ruby
55
+ class User
56
+ include Her::Model
57
+ end
58
+ ```
59
+
60
+ After that, using Her is very similar to many ActiveRecord-like ORMs:
61
+
62
+ ```ruby
63
+ User.all
64
+ # GET "https://api.example.com/users" and return an array of User objects
65
+
66
+ User.find(1)
67
+ # GET "https://api.example.com/users/1" and return a User object
68
+
69
+ @user = User.create(fullname: "Tobias Fünke")
70
+ # POST "https://api.example.com/users" with `fullname=Tobias+Fünke` and return the saved User object
71
+
72
+ @user = User.new(fullname: "Tobias Fünke")
73
+ @user.occupation = "actor"
74
+ @user.save
75
+ # POST "https://api.example.com/users" with `fullname=Tobias+Fünke&occupation=actor` and return the saved User object
76
+
77
+ @user = User.find(1)
78
+ @user.fullname = "Lindsay Fünke"
79
+ @user.save
80
+ # PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object
81
+ ```
82
+
83
+ ### ActiveRecord-like methods
84
+
85
+ These are the basic ActiveRecord-like methods you can use with your models:
86
+
87
+ ```ruby
88
+ class User
89
+ include Her::Model
90
+ end
91
+
92
+ # Update a fetched resource
93
+ user = User.find(1)
94
+ user.fullname = "Lindsay Fünke" # OR user.assign_attributes(fullname: "Lindsay Fünke")
95
+ user.save # returns false if it fails, errors in user.response_errors array
96
+ # PUT "/users/1" with `fullname=Lindsay+Fünke`
97
+
98
+ # Update a resource without fetching it
99
+ User.save_existing(1, fullname: "Lindsay Fünke")
100
+ # PUT "/users/1" with `fullname=Lindsay+Fünke`
101
+
102
+ # Destroy a fetched resource
103
+ user = User.find(1)
104
+ user.destroy
105
+ # DELETE "/users/1"
106
+
107
+ # Destroy a resource without fetching it
108
+ User.destroy_existing(1)
109
+ # DELETE "/users/1"
110
+
111
+ # Fetching a collection of resources
112
+ User.all
113
+ # GET "/users"
114
+ User.where(moderator: 1).all
115
+ # GET "/users?moderator=1"
116
+
117
+ # Create a new resource
118
+ User.create(fullname: "Maeby Fünke")
119
+ # POST "/users" with `fullname=Maeby+Fünke`
120
+
121
+ # Save a new resource
122
+ user = User.new(fullname: "Maeby Fünke")
123
+ user.save! # raises Her::Errors::ResourceInvalid if it fails
124
+ # POST "/users" with `fullname=Maeby+Fünke`
125
+ ```
126
+
127
+ You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her.
128
+
129
+ ## Middleware
130
+
131
+ Since Her relies on [Faraday](https://github.com/lostisland/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.
132
+
133
+ ### Authentication
134
+
135
+ Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `setup` block, we can add it to the middleware stack.
136
+
137
+ For example, to add a token header to your API requests in a Rails application, you could use the excellent [`request_store`](https://rubygems.org/gems/request_store) gem like this:
138
+
139
+ ```ruby
140
+ # app/controllers/application_controller.rb
141
+ class ApplicationController < ActionController::Base
142
+ before_filter :set_user_api_token
143
+
144
+ protected
145
+ def set_user_api_token
146
+ RequestStore.store[:my_api_token] = current_user.api_token # or something similar based on `session`
147
+ end
148
+ end
149
+
150
+ # lib/my_token_authentication.rb
151
+ class MyTokenAuthentication < Faraday::Middleware
152
+ def call(env)
153
+ env[:request_headers]["X-API-Token"] = RequestStore.store[:my_api_token]
154
+ @app.call(env)
155
+ end
156
+ end
157
+
158
+ # config/initializers/her.rb
159
+ require "lib/my_token_authentication"
160
+
161
+ Her::API.setup url: "https://api.example.com" do |c|
162
+ # Request
163
+ c.use MyTokenAuthentication
164
+ c.use Faraday::Request::UrlEncoded
165
+
166
+ # Response
167
+ c.use Her::Middleware::DefaultParseJSON
168
+
169
+ # Adapter
170
+ c.use Faraday::Adapter::NetHttp
171
+ end
172
+ ```
173
+
174
+ Now, each HTTP request made by Her will have the `X-API-Token` header.
175
+
176
+ ### OAuth
177
+
178
+ Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
179
+
180
+ In your Gemfile:
181
+
182
+ ```ruby
183
+ gem "her"
184
+ gem "faraday_middleware"
185
+ gem "simple_oauth"
186
+ ```
187
+
188
+ In your Ruby code:
189
+
190
+ ```ruby
191
+ # Create an application on `https://dev.twitter.com/apps` to set these values
192
+ TWITTER_CREDENTIALS = {
193
+ consumer_key: "",
194
+ consumer_secret: "",
195
+ token: "",
196
+ token_secret: ""
197
+ }
198
+
199
+ Her::API.setup url: "https://api.twitter.com/1/" do |c|
200
+ # Request
201
+ c.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
202
+
203
+ # Response
204
+ c.use Her::Middleware::DefaultParseJSON
205
+
206
+ # Adapter
207
+ c.use Faraday::Adapter::NetHttp
208
+ end
209
+
210
+ class Tweet
211
+ include Her::Model
212
+ end
213
+
214
+ @tweets = Tweet.get("/statuses/home_timeline.json")
215
+ ```
216
+
217
+ See the *Authentication* middleware section for an example of how to pass different credentials based on the current user.
218
+
219
+ ### Parsing JSON data
220
+
221
+ By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
222
+
223
+ ```javascript
224
+ // The response of GET /users/1
225
+ { "id" : 1, "name" : "Tobias Fünke" }
226
+
227
+ // The response of GET /users
228
+ [{ "id" : 1, "name" : "Tobias Fünke" }]
229
+ ```
230
+
231
+ 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).
232
+
233
+ Also, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three symbol keys: `:data`, `:errors` and `:metadata`. The following code uses a custom middleware to parse the JSON data:
234
+
235
+ ```ruby
236
+ # Expects responses like:
237
+ #
238
+ # {
239
+ # "result": { "id": 1, "name": "Tobias Fünke" },
240
+ # "errors": []
241
+ # }
242
+ #
243
+ class MyCustomParser < Faraday::Response::Middleware
244
+ def on_complete(env)
245
+ json = MultiJson.load(env[:body], symbolize_keys: true)
246
+ env[:body] = {
247
+ data: json[:result],
248
+ errors: json[:errors],
249
+ metadata: json[:metadata]
250
+ }
251
+ end
252
+ end
253
+
254
+ Her::API.setup url: "https://api.example.com" do |c|
255
+ # Response
256
+ c.use MyCustomParser
257
+
258
+ # Adapter
259
+ c.use Faraday::Adapter::NetHttp
260
+ end
261
+ ```
262
+
263
+ ### Caching
264
+
265
+ Again, using the `faraday_middleware` and `memcached` gems makes it very easy to cache requests and responses.
266
+
267
+ In your Gemfile:
268
+
269
+ ```ruby
270
+ gem "her"
271
+ gem "faraday_middleware"
272
+ gem "memcached"
273
+ ```
274
+
275
+ In your Ruby code:
276
+
277
+ ```ruby
278
+ Her::API.setup url: "https://api.example.com" do |c|
279
+ # Request
280
+ c.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
281
+
282
+ # Response
283
+ c.use Her::Middleware::DefaultParseJSON
284
+
285
+ # Adapter
286
+ c.use Faraday::Adapter::NetHttp
287
+ end
288
+
289
+ class User
290
+ include Her::Model
291
+ end
292
+
293
+ @user = User.find(1)
294
+ # GET "/users/1"
295
+
296
+ @user = User.find(1)
297
+ # This request will be fetched from memcached
298
+ ```
299
+
300
+ ## Advanced Features
301
+
302
+ Here’s a list of several useful features available in Her.
303
+
304
+ ### Associations
305
+
306
+ Examples use this code:
307
+
308
+ ```ruby
309
+ class User
310
+ include Her::Model
311
+ has_many :comments
312
+ has_one :role
313
+ belongs_to :organization
314
+ end
315
+
316
+ class Comment
317
+ include Her::Model
318
+ end
319
+
320
+ class Role
321
+ include Her::Model
322
+ end
323
+
324
+ class Organization
325
+ include Her::Model
326
+ end
327
+ ```
328
+
329
+ #### Fetching data
330
+
331
+ You can define `has_many`, `has_one` and `belongs_to` associations in your models. The association data is handled in two different ways.
332
+
333
+ 1. If Her finds association data when parsing a resource, that data will be used to create the associated model objects on the resource.
334
+ 2. If no association data was included when parsing a resource, calling a method with the same name as the association will fetch the data (providing there’s an HTTP request available for it in the API).
335
+
336
+ For example, if there’s association data in the resource, no extra HTTP request is made when calling the `#comments` method and an array of resources is returned:
337
+
338
+ ```ruby
339
+ @user = User.find(1)
340
+ # GET "/users/1", response is:
341
+ # {
342
+ # "id": 1,
343
+ # "name": "George Michael Bluth",
344
+ # "comments": [
345
+ # { "id": 1, "text": "Foo" },
346
+ # { "id": 2, "text": "Bar" }
347
+ # ],
348
+ # "role": { "id": 1, "name": "Admin" },
349
+ # "organization": { "id": 2, "name": "Bluth Company" }
350
+ # }
351
+
352
+ @user.comments
353
+ # => [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
354
+
355
+ @user.role
356
+ # => #<Role id=1 name="Admin">
357
+
358
+ @user.organization
359
+ # => #<Organization id=2 name="Bluth Company">
360
+ ```
361
+
362
+ If there’s no association data in the resource, Her makes a HTTP request to retrieve the data.
363
+
364
+ ```ruby
365
+ @user = User.find(1)
366
+ # GET "/users/1", response is { "id": 1, "name": "George Michael Bluth", "organization_id": 2 }
367
+
368
+ # has_many association:
369
+ @user.comments
370
+ # GET "/users/1/comments"
371
+ # => [#<Comment id=1>, #<Comment id=2>]
372
+
373
+ @user.comments.where(approved: 1)
374
+ # GET "/users/1/comments?approved=1"
375
+ # => [#<Comment id=1>]
376
+
377
+ # has_one association:
378
+ @user.role
379
+ # GET "/users/1/role"
380
+ # => #<Role id=1>
381
+
382
+ # belongs_to association:
383
+ @user.organization
384
+ # (the organization id comes from :organization_id, by default)
385
+ # GET "/organizations/2"
386
+ # => #<Organization id=2>
387
+ ```
388
+
389
+ Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
390
+
391
+ #### Creating data
392
+
393
+ You can use the association methods to build new objects and save them.
394
+
395
+ ```ruby
396
+ @user = User.find(1)
397
+ @user.comments.build(body: "Just a draft")
398
+ # => [#<Comment body="Just a draft" user_id=1>]
399
+
400
+ @user.comments.create(body: "Hello world.")
401
+ # POST "/users/1/comments" with `body=Hello+world.`
402
+ # => [#<Comment id=3 body="Hello world." user_id=1>]
403
+ ```
404
+
405
+ You can also explicitly request a new object via the API when using ``build``. This is useful if you're dealing with default attributes.
406
+
407
+ ```ruby
408
+ class Comment
409
+ include Her::Model
410
+ request_new_object_on_build true
411
+ end
412
+
413
+ @user = User.find(1)
414
+ @user.comments.build(body: "Just a draft")
415
+ # GET "/users/1/comments/new" with `body=Just+a+draft.`
416
+ # => [#<Comment id=nil body="Just a draft" archived=false user_id=1>]
417
+ ```
418
+
419
+ #### Notes about paths
420
+
421
+ Resources must always have all the required attributes to build their complete path. For example, if you have these models:
422
+
423
+ ```ruby
424
+ class User
425
+ include Her::Model
426
+ collection_path "organizations/:organization_id/users"
427
+ end
428
+
429
+ class Organization
430
+ include Her::Model
431
+ has_many :users
432
+ end
433
+ ```
434
+
435
+ Her expects all `User` resources to have an `:organization_id` (or `:_organization_id`) attribute. Otherwise, calling mostly all methods, like `User.all`, will thrown an exception like this one:
436
+
437
+ ```ruby
438
+ Her::Errors::PathError: Missing :_organization_id parameter to build the request path. Path is `organizations/:organization_id/users`. Parameters are `{ … }`.
439
+ ```
440
+
441
+ ### Validations
442
+
443
+ Her includes `ActiveModel::Validations` so you can declare validations the same way you do in Rails.
444
+
445
+ However, validations must be triggered manually — they are not run, for example, when calling `#save` on an object, or `#create` on a model class.
446
+
447
+ ```ruby
448
+ class User
449
+ include Her::Model
450
+
451
+ attributes :fullname, :email
452
+ validates :fullname, presence: true
453
+ validates :email, presence: true
454
+ end
455
+
456
+ @user = User.new(fullname: "Tobias Fünke")
457
+ @user.valid? # => false
458
+
459
+ @user.save
460
+ # POST "/users" with `fullname=Tobias+Fünke` will still be called, even if the user is not valid
461
+ ```
462
+
463
+ ### Dirty attributes
464
+
465
+ Her includes `ActiveModel::Dirty` so you can keep track of the attributes that have changed in an object.
466
+
467
+ ```ruby
468
+ class User
469
+ include Her::Model
470
+
471
+ attributes :fullname, :email
472
+ end
473
+
474
+ @user = User.new(fullname: "Tobias Fünke")
475
+ @user.fullname_changed? # => true
476
+ @user.changes # => { :fullname => [nil, "Tobias Fünke"] }
477
+
478
+ @user.save
479
+ # POST "/users" with `fullname=Tobias+Fünke`
480
+
481
+ @user.fullname_changed? # => false
482
+ @user.changes # => {}
483
+ ```
484
+
485
+ To update only the modified attributes specify `:send_only_modified_attributes => true` in the setup.
486
+
487
+ ### Callbacks
488
+
489
+ You can add *before* and *after* callbacks to your models that are triggered on specific actions. You can use symbols or blocks.
490
+
491
+ ```ruby
492
+ class User
493
+ include Her::Model
494
+ before_save :set_internal_id
495
+ after_find { |u| u.fullname.upcase! }
496
+
497
+ def set_internal_id
498
+ self.internal_id = 42 # Will be passed in the HTTP request
499
+ end
500
+ end
501
+
502
+ @user = User.create(fullname: "Tobias Fünke")
503
+ # POST "/users" with `fullname=Tobias+Fünke&internal_id=42`
504
+
505
+ @user = User.find(1)
506
+ @user.fullname # => "TOBIAS FUNKE"
507
+ ```
508
+
509
+ The available callbacks are:
510
+
511
+ * `before_save`
512
+ * `before_create`
513
+ * `before_update`
514
+ * `before_destroy`
515
+ * `after_save`
516
+ * `after_create`
517
+ * `after_update`
518
+ * `after_destroy`
519
+ * `after_find`
520
+ * `after_initialize`
521
+
522
+ ### JSON attributes-wrapping
523
+
524
+ Her supports *sending* and *parsing* JSON data wrapped in a root element (to be compatible with Rails’ `include_root_in_json` setting), like so:
525
+
526
+ #### Sending
527
+
528
+ If you want to send all data to your API wrapped in a *root* element based on the model name.
529
+
530
+ ```ruby
531
+ class User
532
+ include Her::Model
533
+ include_root_in_json true
534
+ end
535
+
536
+ class Article
537
+ include Her::Model
538
+ include_root_in_json :post
539
+ end
540
+
541
+ User.create(fullname: "Tobias Fünke")
542
+ # POST "/users" with `user[fullname]=Tobias+Fünke`
543
+
544
+ Article.create(title: "Hello world.")
545
+ # POST "/articles" with `post[title]=Hello+world`
546
+ ```
547
+
548
+ #### Parsing
549
+
550
+ If the API returns data wrapped in a *root* element based on the model name.
551
+
552
+ ```ruby
553
+ class User
554
+ include Her::Model
555
+ parse_root_in_json true
556
+ end
557
+
558
+ class Article
559
+ include Her::Model
560
+ parse_root_in_json :post
561
+ end
562
+
563
+ user = User.create(fullname: "Tobias Fünke")
564
+ # POST "/users" with `fullname=Tobias+Fünke`, response is { "user": { "fullname": "Tobias Fünke" } }
565
+ user.fullname # => "Tobias Fünke"
566
+
567
+ article = Article.create(title: "Hello world.")
568
+ # POST "/articles" with `title=Hello+world.`, response is { "post": { "title": "Hello world." } }
569
+ article.title # => "Hello world."
570
+ ```
571
+
572
+ Of course, you can use both `include_root_in_json` and `parse_root_in_json` at the same time.
573
+
574
+ #### ActiveModel::Serializers support
575
+
576
+ If the API returns data in the default format used by the
577
+ [ActiveModel::Serializers](https://github.com/rails-api/active_model_serializers)
578
+ project you need to configure Her as follows:
579
+
580
+ ```ruby
581
+ class User
582
+ include Her::Model
583
+ parse_root_in_json true, format: :active_model_serializers
584
+ end
585
+
586
+ user = Users.find(1)
587
+ # GET "/users/1", response is { "user": { "id": 1, "fullname": "Lindsay Fünke" } }
588
+
589
+ users = Users.all
590
+ # GET "/users", response is { "users": [{ "id": 1, "fullname": "Lindsay Fünke" }, { "id": 1, "fullname": "Tobias Fünke" }] }
591
+ ```
592
+
593
+ #### JSON API support
594
+
595
+ To consume a JSON API 1.0 compliant service, it must return data in accordance with the [JSON API spec](http://jsonapi.org/). The general format
596
+ of the data is as follows:
597
+
598
+ ```json
599
+ { "data": {
600
+ "type": "developers",
601
+ "id": "6ab79c8c-ec5a-4426-ad38-8763bbede5a7",
602
+ "attributes": {
603
+ "language": "ruby",
604
+ "name": "avdi grimm",
605
+ }
606
+ }
607
+ ```
608
+
609
+ Then to setup your models:
610
+
611
+ ```ruby
612
+ class Contributor
613
+ include Her::JsonApi::Model
614
+
615
+ # defaults to demodulized, pluralized class name, e.g. contributors
616
+ type :developers
617
+ end
618
+ ```
619
+
620
+ Finally, you'll need to use the included JsonApiParser Her middleware:
621
+
622
+ ```ruby
623
+ Her::API.setup url: 'https://my_awesome_json_api_service' do |c|
624
+ # Request
625
+ c.use FaradayMiddleware::EncodeJson
626
+
627
+ # Response
628
+ c.use Her::Middleware::JsonApiParser
629
+
630
+ # Adapter
631
+ c.use Faraday::Adapter::NetHttp
632
+ end
633
+ ```
634
+
635
+ ### Custom requests
636
+
637
+ You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
638
+
639
+ ```ruby
640
+ class User
641
+ include Her::Model
642
+
643
+ custom_get :popular, :unpopular
644
+ custom_post :from_default
645
+ end
646
+
647
+ User.popular
648
+ # GET "/users/popular"
649
+ # => [#<User id=1>, #<User id=2>]
650
+
651
+ User.unpopular
652
+ # GET "/users/unpopular"
653
+ # => [#<User id=3>, #<User id=4>]
654
+
655
+ User.from_default(name: "Maeby Fünke")
656
+ # POST "/users/from_default" with `name=Maeby+Fünke`
657
+ # => #<User id=5 name="Maeby Fünke">
658
+ ```
659
+
660
+ You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
661
+
662
+ ```ruby
663
+ class User
664
+ include Her::Model
665
+ end
666
+
667
+ User.get(:popular)
668
+ # GET "/users/popular"
669
+ # => [#<User id=1>, #<User id=2>]
670
+
671
+ User.get(:single_best)
672
+ # GET "/users/single_best"
673
+ # => #<User id=1>
674
+ ```
675
+
676
+ You can also use `get_raw` which yields the parsed data and the raw response from the HTTP request. Other HTTP methods are supported (`post_raw`, `put_raw`, etc.).
677
+
678
+ ```ruby
679
+ class User
680
+ include Her::Model
681
+
682
+ def self.total
683
+ get_raw(:stats) do |parsed_data, response|
684
+ parsed_data[:data][:total_users]
685
+ end
686
+ end
687
+ end
688
+
689
+ User.total
690
+ # GET "/users/stats"
691
+ # => 42
692
+ ```
693
+
694
+ You can also use full request paths (with strings instead of symbols).
695
+
696
+ ```ruby
697
+ class User
698
+ include Her::Model
699
+ end
700
+
701
+ User.get("/users/popular")
702
+ # GET "/users/popular"
703
+ # => [#<User id=1>, #<User id=2>]
704
+ ```
705
+
706
+ ### Custom paths
707
+
708
+ You can define custom HTTP paths for your models:
709
+
710
+ ```ruby
711
+ class User
712
+ include Her::Model
713
+ collection_path "/hello_users/:id"
714
+ end
715
+
716
+ @user = User.find(1)
717
+ # GET "/hello_users/1"
718
+ ```
719
+
720
+ You can also include custom variables in your paths:
721
+
722
+ ```ruby
723
+ class User
724
+ include Her::Model
725
+ collection_path "/organizations/:organization_id/users"
726
+ end
727
+
728
+ @user = User.find(1, _organization_id: 2)
729
+ # GET "/organizations/2/users/1"
730
+
731
+ @user = User.all(_organization_id: 2)
732
+ # GET "/organizations/2/users"
733
+
734
+ @user = User.new(fullname: "Tobias Fünke", organization_id: 2)
735
+ @user.save
736
+ # POST "/organizations/2/users" with `fullname=Tobias+Fünke`
737
+ ```
738
+
739
+ ### Custom primary keys
740
+
741
+ If your record uses an attribute other than `:id` to identify itself, specify it using the `primary_key` method:
742
+
743
+ ```ruby
744
+ class User
745
+ include Her::Model
746
+ primary_key :_id
747
+ end
748
+
749
+ user = User.find("4fd89a42ff204b03a905c535")
750
+ # GET "/users/4fd89a42ff204b03a905c535", response is { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" }
751
+
752
+ user.destroy
753
+ # DELETE "/users/4fd89a42ff204b03a905c535"
754
+ ```
755
+
756
+ ### Inheritance
757
+
758
+ If all your models share the same settings, you might want to make them children of a class and only include `Her::Model` in that class. However, there are a few settings that don’t get passed to the children classes:
759
+
760
+ * `root_element`
761
+ * `collection_path` and `resource_path`
762
+
763
+ Those settings are based on the class name, so you don’t have to redefine them each time you create a new children class (but you still can). Every other setting is inherited from the parent (associations, scopes, JSON settings, etc.).
764
+
765
+ ```ruby
766
+ module MyAPI
767
+ class Model
768
+ include Her::Model
769
+
770
+ parse_root_in_json true
771
+ include_root_in_json true
772
+ end
773
+ end
774
+
775
+ class User < MyAPI::Model
776
+ end
777
+
778
+ User.find(1)
779
+ # GET "/users/1"
780
+ ```
781
+
782
+ ### Scopes
783
+
784
+ Just like with ActiveRecord, you can define named scopes for your models. Scopes are chainable and can be used within other scopes.
785
+
786
+ ```ruby
787
+ class User
788
+ include Her::Model
789
+
790
+ scope :by_role, ->(role) { where(role: role) }
791
+ scope :admins, -> { by_role('admin') }
792
+ scope :active, -> { where(active: 1) }
793
+ end
794
+
795
+ @admins = User.admins
796
+ # GET "/users?role=admin"
797
+
798
+ @moderators = User.by_role('moderator')
799
+ # GET "/users?role=moderator"
800
+
801
+ @active_admins = User.active.admins # @admins.active would have worked here too
802
+ # GET "/users?role=admin&active=1"
803
+ ```
804
+
805
+ A neat trick you can do with scopes is interact with complex paths.
806
+
807
+ ```ruby
808
+ class User
809
+ include Her::Model
810
+
811
+ collection_path "organizations/:organization_id/users"
812
+ scope :for_organization, ->(id) { where(organization_id: id) }
813
+ end
814
+
815
+ @user = User.for_organization(3).find(2)
816
+ # GET "/organizations/3/users/2"
817
+
818
+ @user = User.for_organization(3).create(fullname: "Tobias Fünke")
819
+ # POST "/organizations/3" with `fullname=Tobias+Fünke`
820
+ ```
821
+
822
+ ### Multiple APIs
823
+
824
+ It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
825
+
826
+ ```ruby
827
+ # config/initializers/her.rb
828
+ MY_API = Her::API.new
829
+ MY_API.setup url: "https://my-api.example.com" do |c|
830
+ # Response
831
+ c.use Her::Middleware::DefaultParseJSON
832
+
833
+ # Adapter
834
+ c.use Faraday::Adapter::NetHttp
835
+ end
836
+
837
+ OTHER_API = Her::API.new
838
+ OTHER_API.setup url: "https://other-api.example.com" do |c|
839
+ # Response
840
+ c.use Her::Middleware::DefaultParseJSON
841
+
842
+ # Adapter
843
+ c.use Faraday::Adapter::NetHttp
844
+ end
845
+ ```
846
+
847
+ You can then define which API a model will use:
848
+
849
+ ```ruby
850
+ class User
851
+ include Her::Model
852
+ use_api MY_API
853
+ end
854
+
855
+ class Category
856
+ include Her::Model
857
+ use_api OTHER_API
858
+ end
859
+
860
+ User.all
861
+ # GET "https://my-api.example.com/users"
862
+
863
+ Category.all
864
+ # GET "https://other-api.example.com/categories"
865
+ ```
866
+
867
+ ### SSL
868
+
869
+ When initializing `Her::API`, you can pass any parameter supported by `Faraday.new`. So [to use HTTPS](https://github.com/lostisland/faraday/wiki/Setting-up-SSL-certificates), you can use Faraday’s `:ssl` option.
870
+
871
+ ```ruby
872
+ ssl_options = { ca_path: "/usr/lib/ssl/certs" }
873
+ Her::API.setup url: "https://api.example.com", ssl: ssl_options do |c|
874
+ # Response
875
+ c.use Her::Middleware::DefaultParseJSON
876
+
877
+ # Adapter
878
+ c.use Faraday::Adapter::NetHttp
879
+ end
880
+ ```
881
+
882
+ ## Testing
883
+
884
+ Suppose we have these two models bound to your API:
885
+
886
+ ```ruby
887
+ # app/models/user.rb
888
+ class User
889
+ include Her::Model
890
+ custom_get :popular
891
+ end
892
+
893
+ # app/models/post.rb
894
+ class Post
895
+ include Her::Model
896
+ custom_get :recent, :archived
897
+ end
898
+ ```
899
+
900
+ 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:
901
+
902
+ ```ruby
903
+ # spec/spec_helper.rb
904
+ RSpec.configure do |config|
905
+ config.include(Module.new do
906
+ def stub_api_for(klass)
907
+ klass.use_api (api = Her::API.new)
908
+
909
+ # Here, you would customize this for your own API (URL, middleware, etc)
910
+ # like you have done in your application’s initializer
911
+ api.setup url: "http://api.example.com" do |c|
912
+ c.use Her::Middleware::FirstLevelParseJSON
913
+ c.adapter(:test) { |s| yield(s) }
914
+ end
915
+ end
916
+ end)
917
+ end
918
+ ```
919
+
920
+ Then, in your tests, we can specify what (fake) HTTP requests will return:
921
+
922
+ ```ruby
923
+ # spec/models/user.rb
924
+ describe User do
925
+ before do
926
+ stub_api_for(User) do |stub|
927
+ stub.get("/users/popular") { |env| [200, {}, [{ id: 1, name: "Tobias Fünke" }, { id: 2, name: "Lindsay Fünke" }].to_json] }
928
+ end
929
+ end
930
+
931
+ describe :popular do
932
+ subject { User.popular }
933
+ its(:length) { should == 2 }
934
+ its(:errors) { should be_empty }
935
+ end
936
+ end
937
+ ```
938
+
939
+ We can redefine the API for a model as many times as we want, like for more complex tests:
940
+
941
+ ```ruby
942
+ # spec/models/user.rb
943
+ describe Post do
944
+ describe :recent do
945
+ before do
946
+ stub_api_for(Post) do |stub|
947
+ stub.get("/posts/recent") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] }
948
+ end
949
+ end
950
+
951
+ subject { Post.recent }
952
+ its(:length) { should == 2 }
953
+ its(:errors) { should be_empty }
954
+ end
955
+
956
+ describe :archived do
957
+ before do
958
+ stub_api_for(Post) do |stub|
959
+ stub.get("/posts/archived") { |env| [200, {}, [{ id: 1 }, { id: 2 }].to_json] }
960
+ end
961
+ end
962
+
963
+ subject { Post.archived }
964
+ its(:length) { should == 2 }
965
+ its(:errors) { should be_empty }
966
+ end
967
+ end
968
+ ```
969
+
970
+ ## Upgrade
971
+
972
+ See the [UPGRADE.md](https://github.com/remiprev/her/blob/master/UPGRADE.md) for backward compatibility issues.
973
+
974
+ ## Her IRL
975
+
976
+ Most projects I know that use Her are internal or private projects but here’s a list of public ones:
977
+
978
+ * [tumbz](https://github.com/remiprev/tumbz)
979
+ * [crowdher](https://github.com/simonprev/crowdher)
980
+ * [vodka](https://github.com/magnolia-fan/vodka)
981
+ * [webistrano_cli](https://github.com/chytreg/webistrano_cli)
982
+ * [ASMALLWORLD](https://www.asmallworld.com)
983
+
984
+ ## History
985
+
986
+ 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 (and now removed from Rails 4.0), 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.
987
+
988
+ Most of Her’s core concepts were written on a Saturday morning of April 2012 ([first commit](https://github.com/remiprev/her/commit/689d8e88916dc2ad258e69a2a91a283f061cbef2) at 7am!).
989
+
990
+ ## Contribute
991
+
992
+ Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues). There’s no such thing as a bad pull request — even if it’s for a typo, a small improvement to the code or the documentation!
993
+
994
+ See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices.
995
+
996
+ ### Contributors
997
+
998
+ These [fine folks](https://github.com/remiprev/her/contributors) helped with Her:
999
+
1000
+ * [@jfcixmedia](https://github.com/jfcixmedia)
1001
+ * [@EtienneLem](https://github.com/EtienneLem)
1002
+ * [@rafaelss](https://github.com/rafaelss)
1003
+ * [@tysontate](https://github.com/tysontate)
1004
+ * [@nfo](https://github.com/nfo)
1005
+ * [@simonprevost](https://github.com/simonprevost)
1006
+ * [@jmlacroix](https://github.com/jmlacroix)
1007
+ * [@thomsbg](https://github.com/thomsbg)
1008
+ * [@calmyournerves](https://github.com/calmyournerves)
1009
+ * [@luflux](https://github.com/luxflux)
1010
+ * [@simonc](https://github.com/simonc)
1011
+ * [@pencil](https://github.com/pencil)
1012
+ * [@joanniclaborde](https://github.com/joanniclaborde)
1013
+ * [@seanreads](https://github.com/seanreads)
1014
+ * [@jonkarna](https://github.com/jonkarna)
1015
+ * [@aclevy](https://github.com/aclevy)
1016
+ * [@stevschmid](https://github.com/stevschmid)
1017
+ * [@prognostikos](https://github.com/prognostikos)
1018
+ * [@dturnerTS](https://github.com/dturnerTS)
1019
+ * [@kritik](https://github.com/kritik)
1020
+
1021
+ ## License
1022
+
1023
+ Her is © 2012-2013 [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.