test_track_rails_client 2.0.0 → 3.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (98) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +62 -7
  3. data/Rakefile +8 -4
  4. data/app/models/test_track/analytics/mixpanel_client.rb +2 -5
  5. data/app/models/test_track/analytics/safe_wrapper.rb +4 -4
  6. data/app/models/test_track/config_updater.rb +2 -2
  7. data/app/models/test_track/fake/split_registry.rb +2 -2
  8. data/app/models/test_track/notify_assignment_job.rb +3 -4
  9. data/app/models/test_track/offline_session.rb +6 -4
  10. data/app/models/test_track/remote/split_registry.rb +2 -2
  11. data/app/models/test_track/remote/visitor.rb +2 -2
  12. data/app/models/test_track/session.rb +12 -73
  13. data/app/models/test_track/unsynced_assignments_notifier.rb +2 -4
  14. data/app/models/test_track/vary_dsl.rb +5 -1
  15. data/app/models/test_track/visitor.rb +0 -11
  16. data/lib/test_track_rails_client/version.rb +1 -1
  17. data/vendor/gems/fakeable_her/fakeable_her.gemspec +2 -3
  18. data/vendor/gems/her/her.gemspec +4 -5
  19. data/vendor/gems/her/lib/her/api.rb +1 -1
  20. data/vendor/gems/her/lib/her/model/associations/association_proxy.rb +1 -2
  21. data/vendor/gems/her/lib/her/model/orm.rb +41 -29
  22. data/vendor/gems/her/lib/her/model/parse.rb +1 -0
  23. data/vendor/gems/her/lib/her/model/relation.rb +21 -6
  24. data/vendor/gems/her/lib/her/version.rb +1 -1
  25. metadata +52 -139
  26. data/app/models/test_track/create_alias_job.rb +0 -18
  27. data/vendor/gems/her/CONTRIBUTING.md +0 -26
  28. data/vendor/gems/her/Gemfile +0 -10
  29. data/vendor/gems/her/README.md +0 -1023
  30. data/vendor/gems/her/Rakefile +0 -11
  31. data/vendor/gems/her/UPGRADE.md +0 -101
  32. data/vendor/gems/her/gemfiles/Gemfile.activemodel-3.2.x +0 -7
  33. data/vendor/gems/her/gemfiles/Gemfile.activemodel-4.0 +0 -7
  34. data/vendor/gems/her/gemfiles/Gemfile.activemodel-4.1 +0 -7
  35. data/vendor/gems/her/gemfiles/Gemfile.activemodel-4.2 +0 -7
  36. data/vendor/gems/her/spec/api_spec.rb +0 -114
  37. data/vendor/gems/her/spec/collection_spec.rb +0 -26
  38. data/vendor/gems/her/spec/error_collection_spec.rb +0 -33
  39. data/vendor/gems/her/spec/json_api/model_spec.rb +0 -168
  40. data/vendor/gems/her/spec/middleware/accept_json_spec.rb +0 -10
  41. data/vendor/gems/her/spec/middleware/first_level_parse_json_spec.rb +0 -62
  42. data/vendor/gems/her/spec/middleware/json_api_parser_spec.rb +0 -32
  43. data/vendor/gems/her/spec/middleware/second_level_parse_json_spec.rb +0 -35
  44. data/vendor/gems/her/spec/model/associations/association_proxy_spec.rb +0 -31
  45. data/vendor/gems/her/spec/model/associations_spec.rb +0 -504
  46. data/vendor/gems/her/spec/model/attributes_spec.rb +0 -404
  47. data/vendor/gems/her/spec/model/callbacks_spec.rb +0 -145
  48. data/vendor/gems/her/spec/model/dirty_spec.rb +0 -110
  49. data/vendor/gems/her/spec/model/http_spec.rb +0 -165
  50. data/vendor/gems/her/spec/model/introspection_spec.rb +0 -76
  51. data/vendor/gems/her/spec/model/nested_attributes_spec.rb +0 -134
  52. data/vendor/gems/her/spec/model/orm_spec.rb +0 -791
  53. data/vendor/gems/her/spec/model/parse_spec.rb +0 -372
  54. data/vendor/gems/her/spec/model/paths_spec.rb +0 -347
  55. data/vendor/gems/her/spec/model/relation_spec.rb +0 -226
  56. data/vendor/gems/her/spec/model/validations_spec.rb +0 -42
  57. data/vendor/gems/her/spec/model_spec.rb +0 -31
  58. data/vendor/gems/her/spec/spec_helper.rb +0 -27
  59. data/vendor/gems/her/spec/support/extensions/array.rb +0 -5
  60. data/vendor/gems/her/spec/support/extensions/hash.rb +0 -5
  61. data/vendor/gems/her/spec/support/macros/her_macros.rb +0 -17
  62. data/vendor/gems/her/spec/support/macros/model_macros.rb +0 -36
  63. data/vendor/gems/her/spec/support/macros/request_macros.rb +0 -27
  64. data/vendor/gems/publicsuffix-ruby/CHANGELOG.md +0 -236
  65. data/vendor/gems/publicsuffix-ruby/Gemfile +0 -3
  66. data/vendor/gems/publicsuffix-ruby/LICENSE.txt +0 -22
  67. data/vendor/gems/publicsuffix-ruby/README.md +0 -151
  68. data/vendor/gems/publicsuffix-ruby/Rakefile +0 -109
  69. data/vendor/gems/publicsuffix-ruby/lib/definitions.txt +0 -11467
  70. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/domain.rb +0 -387
  71. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/errors.rb +0 -53
  72. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/list.rb +0 -302
  73. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/rule.rb +0 -373
  74. data/vendor/gems/publicsuffix-ruby/lib/public_suffix/version.rb +0 -23
  75. data/vendor/gems/publicsuffix-ruby/lib/public_suffix.rb +0 -131
  76. data/vendor/gems/publicsuffix-ruby/public_suffix.gemspec +0 -39
  77. data/vendor/gems/publicsuffix-ruby/test/acceptance_test.rb +0 -42
  78. data/vendor/gems/publicsuffix-ruby/test/test_helper.rb +0 -6
  79. data/vendor/gems/publicsuffix-ruby/test/unit/domain_test.rb +0 -170
  80. data/vendor/gems/publicsuffix-ruby/test/unit/errors_test.rb +0 -23
  81. data/vendor/gems/publicsuffix-ruby/test/unit/list_test.rb +0 -179
  82. data/vendor/gems/publicsuffix-ruby/test/unit/public_suffix_test.rb +0 -115
  83. data/vendor/gems/publicsuffix-ruby/test/unit/rule_test.rb +0 -307
  84. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/capybara_configuration.rb +0 -98
  85. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/matchers.rb +0 -151
  86. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rspec_configuration.rb +0 -34
  87. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop/cop/betterment/html_safe.rb +0 -15
  88. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop/cop/betterment/raw.rb +0 -15
  89. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop/cop/betterment/safe_concat.rb +0 -15
  90. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/rubocop.rb +0 -3
  91. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/shared_examples/betterment_application_examples.rb +0 -43
  92. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/shared_examples.rb +0 -1
  93. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/site_prism_configuration.rb +0 -42
  94. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/site_prism_dropdown.rb +0 -17
  95. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/version.rb +0 -3
  96. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers/webmock_configuration.rb +0 -8
  97. data/vendor/gems/ruby_spec_helpers/lib/ruby_spec_helpers.rb +0 -2
  98. data/vendor/gems/ruby_spec_helpers/ruby_spec_helpers.gemspec +0 -25
@@ -1,1023 +0,0 @@
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.