her5 0.8.1

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