her 0.6.5 → 0.6.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +8 -8
- data/README.md +130 -108
- data/lib/her/model.rb +1 -1
- data/lib/her/model/attributes.rb +1 -0
- data/lib/her/model/http.rb +1 -0
- data/lib/her/model/parse.rb +6 -1
- data/lib/her/version.rb +1 -1
- data/spec/model/callbacks_spec.rb +25 -0
- data/spec/model/parse_spec.rb +31 -2
- data/spec/model/relation_spec.rb +19 -0
- data/spec/support/macros/request_macros.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
---
|
2
2
|
!binary "U0hBMQ==":
|
3
3
|
metadata.gz: !binary |-
|
4
|
-
|
4
|
+
NWQ2MzM5ODU1NDgyMmFlODFmNzJiZWRkY2ViMGNhYmY0MDc2YTIyZQ==
|
5
5
|
data.tar.gz: !binary |-
|
6
|
-
|
6
|
+
YTUzYTM2MjY4ZGM2Njc3NDA2ZGRlMDUyYzc4MmNmOGFkODhjNmVlZA==
|
7
7
|
!binary "U0hBNTEy":
|
8
8
|
metadata.gz: !binary |-
|
9
|
-
|
10
|
-
|
11
|
-
|
9
|
+
ZmMyZTY3NTg1ODFjNDBkODYzYWFlZDg4YTZhNTM5Mzk2NzViYzZhZmI3NTQw
|
10
|
+
OTI5ZDQyOWVjZDg5MzRmNGVhMDYzOTcyYTlkM2MwNWI1NzYzYTIyNDU2NGE1
|
11
|
+
YTg3MDY3OWU5NTQxODFlYjhiY2Y0MGYwYTVkMjdlZTg3NzUzZmM=
|
12
12
|
data.tar.gz: !binary |-
|
13
|
-
|
14
|
-
|
15
|
-
|
13
|
+
ZGE4MjRkN2Y1NjFhYjcxNGE5MTI5ODAzMTdjNGZkMDgzZGNmYmRjODExNDBm
|
14
|
+
YTU0NDM4YjQ5ZGFhM2I0MTEyZDZlMTlmOTMxZjY4NjNiODVhYTczMjY1Njc0
|
15
|
+
YzFmMzQxOGU1MmU1ODEyYTY0MDc1YWQxODY2NmMyZmYwYTRlMjQ=
|
data/README.md
CHANGED
@@ -25,10 +25,10 @@ First, you have to define which API your models will be bound to. For example, w
|
|
25
25
|
|
26
26
|
```ruby
|
27
27
|
# config/initializers/her.rb
|
28
|
-
Her::API.setup url: "https://api.example.com" do |
|
29
|
-
|
30
|
-
|
31
|
-
|
28
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
29
|
+
c.use Faraday::Request::UrlEncoded
|
30
|
+
c.use Her::Middleware::DefaultParseJSON
|
31
|
+
c.use Faraday::Adapter::NetHttp
|
32
32
|
end
|
33
33
|
```
|
34
34
|
|
@@ -44,10 +44,10 @@ After that, using Her is very similar to many ActiveRecord-like ORMs:
|
|
44
44
|
|
45
45
|
```ruby
|
46
46
|
User.all
|
47
|
-
# GET https://api.example.com/users and return an array of User objects
|
47
|
+
# GET "https://api.example.com/users" and return an array of User objects
|
48
48
|
|
49
49
|
User.find(1)
|
50
|
-
# GET https://api.example.com/users/1 and return a User object
|
50
|
+
# GET "https://api.example.com/users/1" and return a User object
|
51
51
|
|
52
52
|
@user = User.create(fullname: "Tobias Fünke")
|
53
53
|
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke` and return the saved User object
|
@@ -55,12 +55,12 @@ User.find(1)
|
|
55
55
|
@user = User.new(fullname: "Tobias Fünke")
|
56
56
|
@user.occupation = "actor"
|
57
57
|
@user.save
|
58
|
-
# POST https://api.example.com/users with `fullname=Tobias+Fünke&occupation=
|
58
|
+
# POST "https://api.example.com/users" with `fullname=Tobias+Fünke&occupation=actor` and return the saved User object
|
59
59
|
|
60
60
|
@user = User.find(1)
|
61
61
|
@user.fullname = "Lindsay Fünke"
|
62
62
|
@user.save
|
63
|
-
# PUT https://api.example.com/users/1 with `fullname=Lindsay+Fünke` and return the updated User object
|
63
|
+
# PUT "https://api.example.com/users/1" with `fullname=Lindsay+Fünke` and return the updated User object
|
64
64
|
```
|
65
65
|
|
66
66
|
### ActiveRecord-like methods
|
@@ -74,30 +74,37 @@ end
|
|
74
74
|
|
75
75
|
# Update a fetched resource
|
76
76
|
user = User.find(1)
|
77
|
-
user.fullname = "Lindsay Fünke"
|
78
|
-
# OR user.assign_attributes(fullname: "Lindsay Fünke")
|
77
|
+
user.fullname = "Lindsay Fünke" # OR user.assign_attributes(fullname: "Lindsay Fünke")
|
79
78
|
user.save
|
79
|
+
# PUT "/users/1" with `fullname=Lindsay+Fünke`
|
80
80
|
|
81
81
|
# Update a resource without fetching it
|
82
82
|
User.save_existing(1, fullname: "Lindsay Fünke")
|
83
|
+
# PUT "/users/1" with `fullname=Lindsay+Fünke`
|
83
84
|
|
84
85
|
# Destroy a fetched resource
|
85
86
|
user = User.find(1)
|
86
87
|
user.destroy
|
88
|
+
# DELETE "/users/1"
|
87
89
|
|
88
90
|
# Destroy a resource without fetching it
|
89
91
|
User.destroy_existing(1)
|
92
|
+
# DELETE "/users/1"
|
90
93
|
|
91
94
|
# Fetching a collection of resources
|
92
95
|
User.all
|
96
|
+
# GET "/users"
|
93
97
|
User.where(moderator: 1).all
|
98
|
+
# GET "/users?moderator=1"
|
94
99
|
|
95
100
|
# Create a new resource
|
96
101
|
User.create(fullname: "Maeby Fünke")
|
102
|
+
# POST "/users" with `fullname=Maeby+Fünke`
|
97
103
|
|
98
104
|
# Save a new resource
|
99
105
|
user = User.new(fullname: "Maeby Fünke")
|
100
106
|
user.save
|
107
|
+
# POST "/users" with `fullname=Maeby+Fünke`
|
101
108
|
```
|
102
109
|
|
103
110
|
You can look into the [`her-example`](https://github.com/remiprev/her-example) repository for a sample application using Her.
|
@@ -108,7 +115,7 @@ Since Her relies on [Faraday](https://github.com/lostisland/faraday) to send HTT
|
|
108
115
|
|
109
116
|
### Authentication
|
110
117
|
|
111
|
-
Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `
|
118
|
+
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.
|
112
119
|
|
113
120
|
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:
|
114
121
|
|
@@ -134,11 +141,11 @@ end
|
|
134
141
|
# config/initializers/her.rb
|
135
142
|
require "lib/my_token_authentication"
|
136
143
|
|
137
|
-
Her::API.setup url: "https://api.example.com" do |
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
144
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
145
|
+
c.use MyTokenAuthentication
|
146
|
+
c.use Faraday::Request::UrlEncoded
|
147
|
+
c.use Her::Middleware::DefaultParseJSON
|
148
|
+
c.use Faraday::Adapter::NetHttp
|
142
149
|
end
|
143
150
|
```
|
144
151
|
|
@@ -167,10 +174,10 @@ TWITTER_CREDENTIALS = {
|
|
167
174
|
token_secret: ""
|
168
175
|
}
|
169
176
|
|
170
|
-
Her::API.setup url: "https://api.twitter.com/1/" do |
|
171
|
-
|
172
|
-
|
173
|
-
|
177
|
+
Her::API.setup url: "https://api.twitter.com/1/" do |c|
|
178
|
+
c.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
|
179
|
+
c.use Her::Middleware::DefaultParseJSON
|
180
|
+
c.use Faraday::Adapter::NetHttp
|
174
181
|
end
|
175
182
|
|
176
183
|
class Tweet
|
@@ -202,10 +209,7 @@ Also, you can define your own parsing method using a response middleware. The mi
|
|
202
209
|
# Expects responses like:
|
203
210
|
#
|
204
211
|
# {
|
205
|
-
# "result": {
|
206
|
-
# "id": 1,
|
207
|
-
# "name": "Tobias Fünke"
|
208
|
-
# },
|
212
|
+
# "result": { "id": 1, "name": "Tobias Fünke" },
|
209
213
|
# "errors": []
|
210
214
|
# }
|
211
215
|
#
|
@@ -220,9 +224,9 @@ class MyCustomParser < Faraday::Response::Middleware
|
|
220
224
|
end
|
221
225
|
end
|
222
226
|
|
223
|
-
Her::API.setup url: "https://api.example.com" do |
|
224
|
-
|
225
|
-
|
227
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
228
|
+
c.use MyCustomParser
|
229
|
+
c.use Faraday::Adapter::NetHttp
|
226
230
|
end
|
227
231
|
```
|
228
232
|
|
@@ -241,10 +245,10 @@ gem "memcached"
|
|
241
245
|
In your Ruby code:
|
242
246
|
|
243
247
|
```ruby
|
244
|
-
Her::API.setup url: "https://api.example.com" do |
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
+
Her::API.setup url: "https://api.example.com" do |c|
|
249
|
+
c.use FaradayMiddleware::Caching, Memcached::Rails.new('127.0.0.1:11211')
|
250
|
+
c.use Her::Middleware::DefaultParseJSON
|
251
|
+
c.use Faraday::Adapter::NetHttp
|
248
252
|
end
|
249
253
|
|
250
254
|
class User
|
@@ -252,7 +256,7 @@ class User
|
|
252
256
|
end
|
253
257
|
|
254
258
|
@user = User.find(1)
|
255
|
-
# GET /users/1
|
259
|
+
# GET "/users/1"
|
256
260
|
|
257
261
|
@user = User.find(1)
|
258
262
|
# This request will be fetched from memcached
|
@@ -264,12 +268,7 @@ Here’s a list of several useful features available in Her.
|
|
264
268
|
|
265
269
|
### Associations
|
266
270
|
|
267
|
-
|
268
|
-
|
269
|
-
1. If Her finds association data when parsing a resource, that data will be used to create the associated model objects on the resource.
|
270
|
-
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).
|
271
|
-
|
272
|
-
For example:
|
271
|
+
Examples use this code:
|
273
272
|
|
274
273
|
```ruby
|
275
274
|
class User
|
@@ -292,10 +291,18 @@ class Organization
|
|
292
291
|
end
|
293
292
|
```
|
294
293
|
|
295
|
-
|
294
|
+
#### Fetching data
|
295
|
+
|
296
|
+
You can define `has_many`, `has_one` and `belongs_to` associations in your models. The association data is handled in two different ways.
|
297
|
+
|
298
|
+
1. If Her finds association data when parsing a resource, that data will be used to create the associated model objects on the resource.
|
299
|
+
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).
|
300
|
+
|
301
|
+
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:
|
296
302
|
|
297
303
|
```ruby
|
298
304
|
@user = User.find(1)
|
305
|
+
# GET "/users/1", response is:
|
299
306
|
# {
|
300
307
|
# "id": 1,
|
301
308
|
# "name": "George Michael Bluth",
|
@@ -306,43 +313,60 @@ If there’s association data in the resource, no extra HTTP request is made whe
|
|
306
313
|
# "role": { "id": 1, "name": "Admin" },
|
307
314
|
# "organization": { "id": 2, "name": "Bluth Company" }
|
308
315
|
# }
|
316
|
+
|
309
317
|
@user.comments
|
310
|
-
# [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
|
318
|
+
# => [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
|
319
|
+
|
311
320
|
@user.role
|
312
|
-
# #<Role id=1 name="Admin">
|
321
|
+
# => #<Role id=1 name="Admin">
|
322
|
+
|
313
323
|
@user.organization
|
314
|
-
# #<Organization id=2 name="Bluth Company">
|
324
|
+
# => #<Organization id=2 name="Bluth Company">
|
315
325
|
```
|
316
326
|
|
317
327
|
If there’s no association data in the resource, Her makes a HTTP request to retrieve the data.
|
318
328
|
|
319
329
|
```ruby
|
320
330
|
@user = User.find(1)
|
321
|
-
# { "id": 1, "name": "George Michael Bluth", "organization_id": 2 }
|
331
|
+
# GET "/users/1", response is { "id": 1, "name": "George Michael Bluth", "organization_id": 2 }
|
322
332
|
|
323
333
|
# has_many association:
|
324
334
|
@user.comments
|
325
|
-
# GET /users/1/comments
|
326
|
-
# [#<Comment id=1>, #<Comment id=2>]
|
335
|
+
# GET "/users/1/comments"
|
336
|
+
# => [#<Comment id=1>, #<Comment id=2>]
|
327
337
|
|
328
338
|
@user.comments.where(approved: 1)
|
329
|
-
# GET /users/1/comments?approved=1
|
330
|
-
# [#<Comment id=1>]
|
339
|
+
# GET "/users/1/comments?approved=1"
|
340
|
+
# => [#<Comment id=1>]
|
331
341
|
|
332
342
|
# has_one association:
|
333
343
|
@user.role
|
334
|
-
# GET /users/1/role
|
335
|
-
# #<Role id=1>
|
344
|
+
# GET "/users/1/role"
|
345
|
+
# => #<Role id=1>
|
336
346
|
|
337
347
|
# belongs_to association:
|
338
348
|
@user.organization
|
339
349
|
# (the organization id comes from :organization_id, by default)
|
340
|
-
# GET /organizations/2
|
341
|
-
# #<Organization id=2>
|
350
|
+
# GET "/organizations/2"
|
351
|
+
# => #<Organization id=2>
|
342
352
|
```
|
343
353
|
|
344
354
|
Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
|
345
355
|
|
356
|
+
#### Creating data
|
357
|
+
|
358
|
+
You can use the association methods to build new objects and save them.
|
359
|
+
|
360
|
+
```ruby
|
361
|
+
@user = User.find(1)
|
362
|
+
@user.comments.build(body: "Just a draft")
|
363
|
+
# => [#<Comment body="Just a draft" user_id=1>]
|
364
|
+
|
365
|
+
@user.comments.create(body: "Hello world.")
|
366
|
+
# POST "/users/1/comments" with `body=Hello+world.`
|
367
|
+
# => [#<Comment id=3 body="Hello world." user_id=1>]
|
368
|
+
```
|
369
|
+
|
346
370
|
#### Notes about paths
|
347
371
|
|
348
372
|
Resources must always have all the required attributes to build their complete path. For example, if you have these models:
|
@@ -384,7 +408,7 @@ end
|
|
384
408
|
@user.valid? # => false
|
385
409
|
|
386
410
|
@user.save
|
387
|
-
# POST /users
|
411
|
+
# POST "/users" with `fullname=Tobias+Fünke` will still be called, even if the user is not valid
|
388
412
|
```
|
389
413
|
|
390
414
|
### Dirty attributes
|
@@ -403,7 +427,7 @@ end
|
|
403
427
|
@user.changes # => { :fullname => [nil, "Tobias Fünke"] }
|
404
428
|
|
405
429
|
@user.save
|
406
|
-
# POST /users
|
430
|
+
# POST "/users" with `fullname=Tobias+Fünke`
|
407
431
|
|
408
432
|
@user.fullname_changed? # => false
|
409
433
|
@user.changes # => {}
|
@@ -425,7 +449,7 @@ class User
|
|
425
449
|
end
|
426
450
|
|
427
451
|
@user = User.create(fullname: "Tobias Funke")
|
428
|
-
# POST /users
|
452
|
+
# POST "/users" with `fullname=Tobias+Fünke&internal_id=42`
|
429
453
|
|
430
454
|
@user = User.find(1)
|
431
455
|
@user.fullname # => "TOBIAS FUNKE"
|
@@ -442,6 +466,7 @@ The available callbacks are:
|
|
442
466
|
* `after_update`
|
443
467
|
* `after_destroy`
|
444
468
|
* `after_find`
|
469
|
+
* `after_initialize`
|
445
470
|
|
446
471
|
### JSON attributes-wrapping
|
447
472
|
|
@@ -463,10 +488,10 @@ class Article
|
|
463
488
|
end
|
464
489
|
|
465
490
|
User.create(fullname: "Tobias Fünke")
|
466
|
-
# POST
|
491
|
+
# POST "/users" with `user[fullname]=Tobias+Fünke`
|
467
492
|
|
468
493
|
Article.create(title: "Hello world.")
|
469
|
-
# POST
|
494
|
+
# POST "/articles" with `post[title]=Hello+world`
|
470
495
|
```
|
471
496
|
|
472
497
|
#### Parsing
|
@@ -484,12 +509,12 @@ class Article
|
|
484
509
|
parse_root_in_json :post
|
485
510
|
end
|
486
511
|
|
487
|
-
# POST /users returns { "user": { "fullname": "Tobias Fünke" } }
|
488
512
|
user = User.create(fullname: "Tobias Fünke")
|
513
|
+
# POST "/users" with `fullname=Tobias+Fünke`, response is { "user": { "fullname": "Tobias Fünke" } }
|
489
514
|
user.fullname # => "Tobias Fünke"
|
490
515
|
|
491
|
-
# POST /articles returns { "post": { "title": "Hello world." } }
|
492
516
|
article = Article.create(title: "Hello world.")
|
517
|
+
# POST "/articles" with `title=Hello+world.`, response is { "post": { "title": "Hello world." } }
|
493
518
|
article.title # => "Hello world."
|
494
519
|
```
|
495
520
|
|
@@ -508,16 +533,16 @@ class User
|
|
508
533
|
end
|
509
534
|
|
510
535
|
User.popular
|
511
|
-
# GET /users/popular
|
512
|
-
# [#<User id=1>, #<User id=2>]
|
536
|
+
# GET "/users/popular"
|
537
|
+
# => [#<User id=1>, #<User id=2>]
|
513
538
|
|
514
539
|
User.unpopular
|
515
|
-
# GET /users/unpopular
|
516
|
-
# [#<User id=3>, #<User id=4>]
|
540
|
+
# GET "/users/unpopular"
|
541
|
+
# => [#<User id=3>, #<User id=4>]
|
517
542
|
|
518
543
|
User.from_default(name: "Maeby Fünke")
|
519
|
-
# POST /users/from_default with `name=Maeby+Fünke`
|
520
|
-
# #<User id=5 name="Maeby Fünke">
|
544
|
+
# POST "/users/from_default" with `name=Maeby+Fünke`
|
545
|
+
# => #<User id=5 name="Maeby Fünke">
|
521
546
|
```
|
522
547
|
|
523
548
|
You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
|
@@ -528,24 +553,20 @@ class User
|
|
528
553
|
end
|
529
554
|
|
530
555
|
User.get(:popular)
|
531
|
-
# GET /users/popular
|
532
|
-
# [#<User id=1>, #<User id=2>]
|
556
|
+
# GET "/users/popular"
|
557
|
+
# => [#<User id=1>, #<User id=2>]
|
533
558
|
|
534
559
|
User.get(:single_best)
|
535
|
-
# GET /users/single_best
|
536
|
-
# #<User id=1>
|
560
|
+
# GET "/users/single_best"
|
561
|
+
# => #<User id=1>
|
537
562
|
```
|
538
563
|
|
539
|
-
|
564
|
+
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.).
|
540
565
|
|
541
566
|
```ruby
|
542
567
|
class User
|
543
568
|
include Her::Model
|
544
569
|
|
545
|
-
def self.popular
|
546
|
-
get_collection(:popular)
|
547
|
-
end
|
548
|
-
|
549
570
|
def self.total
|
550
571
|
get_raw(:stats) do |parsed_data, response|
|
551
572
|
parsed_data[:data][:total_users]
|
@@ -553,12 +574,8 @@ class User
|
|
553
574
|
end
|
554
575
|
end
|
555
576
|
|
556
|
-
User.popular
|
557
|
-
# GET /users/popular
|
558
|
-
# [#<User id=1>, #<User id=2>]
|
559
|
-
|
560
577
|
User.total
|
561
|
-
# GET /users/stats
|
578
|
+
# GET "/users/stats"
|
562
579
|
# => 42
|
563
580
|
```
|
564
581
|
|
@@ -570,8 +587,8 @@ class User
|
|
570
587
|
end
|
571
588
|
|
572
589
|
User.get("/users/popular")
|
573
|
-
# GET /users/popular
|
574
|
-
# [#<User id=1>, #<User id=2>]
|
590
|
+
# GET "/users/popular"
|
591
|
+
# => [#<User id=1>, #<User id=2>]
|
575
592
|
```
|
576
593
|
|
577
594
|
### Custom paths
|
@@ -585,7 +602,7 @@ class User
|
|
585
602
|
end
|
586
603
|
|
587
604
|
@user = User.find(1)
|
588
|
-
# GET /hello_users/1
|
605
|
+
# GET "/hello_users/1"
|
589
606
|
```
|
590
607
|
|
591
608
|
You can also include custom variables in your paths:
|
@@ -597,14 +614,14 @@ class User
|
|
597
614
|
end
|
598
615
|
|
599
616
|
@user = User.find(1, _organization_id: 2)
|
600
|
-
# GET /organizations/2/users/1
|
617
|
+
# GET "/organizations/2/users/1"
|
601
618
|
|
602
619
|
@user = User.all(_organization_id: 2)
|
603
|
-
# GET /organizations/2/users
|
620
|
+
# GET "/organizations/2/users"
|
604
621
|
|
605
622
|
@user = User.new(fullname: "Tobias Fünke", organization_id: 2)
|
606
623
|
@user.save
|
607
|
-
# POST /organizations/2/users with `fullname=Tobias+Fünke`
|
624
|
+
# POST "/organizations/2/users" with `fullname=Tobias+Fünke`
|
608
625
|
```
|
609
626
|
|
610
627
|
### Custom primary keys
|
@@ -617,8 +634,11 @@ class User
|
|
617
634
|
primary_key :_id
|
618
635
|
end
|
619
636
|
|
620
|
-
user = User.find("4fd89a42ff204b03a905c535")
|
621
|
-
|
637
|
+
user = User.find("4fd89a42ff204b03a905c535")
|
638
|
+
# GET "/users/1", response is { "_id": "4fd89a42ff204b03a905c535", "name": "Tobias" }
|
639
|
+
|
640
|
+
user.destroy
|
641
|
+
# DELETE "/users/4fd89a42ff204b03a905c535"
|
622
642
|
```
|
623
643
|
|
624
644
|
### Inheritance
|
@@ -644,7 +664,7 @@ class User < MyAPI::Model
|
|
644
664
|
end
|
645
665
|
|
646
666
|
User.find(1)
|
647
|
-
# GET /users/1
|
667
|
+
# GET "/users/1"
|
648
668
|
```
|
649
669
|
|
650
670
|
### Scopes
|
@@ -661,13 +681,13 @@ class User
|
|
661
681
|
end
|
662
682
|
|
663
683
|
@admins = User.admins
|
664
|
-
# GET /users?role=admin
|
684
|
+
# GET "/users?role=admin"
|
665
685
|
|
666
686
|
@moderators = User.by_role('moderator')
|
667
|
-
# GET /users?role=moderator
|
687
|
+
# GET "/users?role=moderator"
|
668
688
|
|
669
689
|
@active_admins = User.active.admins # @admins.active would have worked here too
|
670
|
-
# GET /users?role=admin&active=1
|
690
|
+
# GET "/users?role=admin&active=1"
|
671
691
|
```
|
672
692
|
|
673
693
|
A neat trick you can do with scopes is interact with complex paths.
|
@@ -681,10 +701,10 @@ class User
|
|
681
701
|
end
|
682
702
|
|
683
703
|
@user = User.for_organization(3).find(2)
|
684
|
-
# GET /organizations/3/users/2
|
704
|
+
# GET "/organizations/3/users/2"
|
685
705
|
|
686
706
|
@user = User.for_organization(3).create(fullname: "Tobias Fünke")
|
687
|
-
# POST /organizations/3 with `fullname=Tobias+Fünke`
|
707
|
+
# POST "/organizations/3" with `fullname=Tobias+Fünke`
|
688
708
|
```
|
689
709
|
|
690
710
|
### Multiple APIs
|
@@ -694,15 +714,15 @@ It is possible to use different APIs for different models. Instead of calling `H
|
|
694
714
|
```ruby
|
695
715
|
# config/initializers/her.rb
|
696
716
|
MY_API = Her::API.new
|
697
|
-
MY_API.setup url: "https://my-api.example.com" do |
|
698
|
-
|
699
|
-
|
717
|
+
MY_API.setup url: "https://my-api.example.com" do |c|
|
718
|
+
c.use Her::Middleware::DefaultParseJSON
|
719
|
+
c.use Faraday::Adapter::NetHttp
|
700
720
|
end
|
701
721
|
|
702
722
|
OTHER_API = Her::API.new
|
703
|
-
OTHER_API.setup url: "https://other-api.example.com" do |
|
704
|
-
|
705
|
-
|
723
|
+
OTHER_API.setup url: "https://other-api.example.com" do |c|
|
724
|
+
c.use Her::Middleware::DefaultParseJSON
|
725
|
+
c.use Faraday::Adapter::NetHttp
|
706
726
|
end
|
707
727
|
```
|
708
728
|
|
@@ -720,10 +740,10 @@ class Category
|
|
720
740
|
end
|
721
741
|
|
722
742
|
User.all
|
723
|
-
# GET https://my-api.example.com/users
|
743
|
+
# GET "https://my-api.example.com/users"
|
724
744
|
|
725
745
|
Category.all
|
726
|
-
# GET https://other-api.example.com/categories
|
746
|
+
# GET "https://other-api.example.com/categories"
|
727
747
|
```
|
728
748
|
|
729
749
|
### SSL
|
@@ -732,9 +752,9 @@ When initializing `Her::API`, you can pass any parameter supported by `Faraday.n
|
|
732
752
|
|
733
753
|
```ruby
|
734
754
|
ssl_options = { ca_path: "/usr/lib/ssl/certs" }
|
735
|
-
Her::API.setup url: "https://api.example.com", ssl: ssl_options do |
|
736
|
-
|
737
|
-
|
755
|
+
Her::API.setup url: "https://api.example.com", ssl: ssl_options do |c|
|
756
|
+
c.use Her::Middleware::DefaultParseJSON
|
757
|
+
c.use Faraday::Adapter::NetHttp
|
738
758
|
end
|
739
759
|
```
|
740
760
|
|
@@ -767,9 +787,9 @@ RSpec.configure do |config|
|
|
767
787
|
|
768
788
|
# Here, you would customize this for your own API (URL, middleware, etc)
|
769
789
|
# like you have done in your application’s initializer
|
770
|
-
api.setup url: "http://api.example.com" do |
|
771
|
-
|
772
|
-
|
790
|
+
api.setup url: "http://api.example.com" do |c|
|
791
|
+
c.use Her::Middleware::FirstLevelParseJSON
|
792
|
+
c.adapter(:test) { |s| yield(s) }
|
773
793
|
end
|
774
794
|
end
|
775
795
|
end)
|
@@ -836,16 +856,18 @@ Most projects I know that use Her are internal or private projects but here’s
|
|
836
856
|
|
837
857
|
* [tumbz](https://github.com/remiprev/tumbz)
|
838
858
|
* [crowdher](https://github.com/simonprev/crowdher)
|
859
|
+
* [vodka](https://github.com/magnolia-fan/vodka)
|
860
|
+
* [webistrano_cli](https://github.com/chytreg/webistrano_cli)
|
839
861
|
|
840
862
|
## History
|
841
863
|
|
842
864
|
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.
|
843
865
|
|
844
|
-
Most of Her’s core
|
866
|
+
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!).
|
845
867
|
|
846
868
|
## Contribute
|
847
869
|
|
848
|
-
Yes please! Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues).
|
870
|
+
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!
|
849
871
|
|
850
872
|
See [CONTRIBUTING.md](https://github.com/remiprev/her/blob/master/CONTRIBUTING.md) for best practices.
|
851
873
|
|
data/lib/her/model.rb
CHANGED
data/lib/her/model/attributes.rb
CHANGED
data/lib/her/model/http.rb
CHANGED
@@ -62,6 +62,7 @@ module Her
|
|
62
62
|
class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
63
63
|
def #{method}(path, params={})
|
64
64
|
path = build_request_path_from_string_or_symbol(path, params)
|
65
|
+
params = to_params(params) unless #{method.to_sym.inspect} == :get
|
65
66
|
send(:'#{method}_raw', path, params) do |parsed_data, response|
|
66
67
|
if parsed_data[:data].is_a?(Array)
|
67
68
|
new_collection(parsed_data)
|
data/lib/her/model/parse.rb
CHANGED
@@ -10,7 +10,7 @@ module Her
|
|
10
10
|
# @user.to_params
|
11
11
|
# # => { :id => 1, :name => 'John Smith' }
|
12
12
|
def to_params
|
13
|
-
self.class.
|
13
|
+
self.class.to_params(self.attributes)
|
14
14
|
end
|
15
15
|
|
16
16
|
module ClassMethods
|
@@ -22,6 +22,11 @@ module Her
|
|
22
22
|
parse_root_in_json? ? data[parsed_root_element] : data
|
23
23
|
end
|
24
24
|
|
25
|
+
# @private
|
26
|
+
def to_params(attributes)
|
27
|
+
include_root_in_json? ? { included_root_element => attributes.dup.symbolize_keys } : attributes.dup.symbolize_keys
|
28
|
+
end
|
29
|
+
|
25
30
|
# Return or change the value of `include_root_in_json`
|
26
31
|
#
|
27
32
|
# @example
|
data/lib/her/version.rb
CHANGED
@@ -117,4 +117,29 @@ describe "Her::Model and ActiveModel::Callbacks" do
|
|
117
117
|
its(:name) { should == "TOBIAS FUNKE" }
|
118
118
|
end
|
119
119
|
end
|
120
|
+
|
121
|
+
context :after_initialize do
|
122
|
+
subject { Foo::User.new(:name => "Tobias Funke") }
|
123
|
+
|
124
|
+
context "when using a symbol callback" do
|
125
|
+
before do
|
126
|
+
class Foo::User
|
127
|
+
after_initialize :alter_name
|
128
|
+
def alter_name; self.name.upcase!; end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
its(:name) { should == "TOBIAS FUNKE" }
|
133
|
+
end
|
134
|
+
|
135
|
+
context "when using a block callback" do
|
136
|
+
before do
|
137
|
+
class Foo::User
|
138
|
+
after_initialize lambda { self.name.upcase! }
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
its(:name) { should == "TOBIAS FUNKE" }
|
143
|
+
end
|
144
|
+
end
|
120
145
|
end
|
data/spec/model/parse_spec.rb
CHANGED
@@ -3,23 +3,43 @@ require File.join(File.dirname(__FILE__), "../spec_helper.rb")
|
|
3
3
|
|
4
4
|
describe Her::Model::Parse do
|
5
5
|
context "when include_root_in_json is set" do
|
6
|
+
before do
|
7
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
8
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
9
|
+
builder.use Faraday::Request::UrlEncoded
|
10
|
+
end
|
11
|
+
|
12
|
+
Her::API.default_api.connection.adapter :test do |stub|
|
13
|
+
stub.post("/users") { |env| [200, {}, { :user => { :id => 1, :fullname => params(env)[:user][:fullname] } }.to_json] }
|
14
|
+
stub.post("/users/admins") { |env| [200, {}, { :user => { :id => 1, :fullname => params(env)[:user][:fullname] } }.to_json] }
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
6
18
|
context "to true" do
|
7
19
|
before do
|
8
20
|
spawn_model "Foo::User" do
|
9
21
|
include_root_in_json true
|
22
|
+
parse_root_in_json true
|
23
|
+
custom_post :admins
|
10
24
|
end
|
11
25
|
end
|
12
26
|
|
13
|
-
it "wraps params in the element name" do
|
27
|
+
it "wraps params in the element name in `to_params`" do
|
14
28
|
@new_user = Foo::User.new(:fullname => "Tobias Fünke")
|
15
29
|
@new_user.to_params.should == { :user => { :fullname => "Tobias Fünke" } }
|
16
30
|
end
|
31
|
+
|
32
|
+
it "wraps params in the element name in `.create`" do
|
33
|
+
@new_user = Foo::User.admins(:fullname => "Tobias Fünke")
|
34
|
+
@new_user.fullname.should == "Tobias Fünke"
|
35
|
+
end
|
17
36
|
end
|
18
37
|
|
19
38
|
context "to a symbol" do
|
20
39
|
before do
|
21
40
|
spawn_model "Foo::User" do
|
22
41
|
include_root_in_json :person
|
42
|
+
parse_root_in_json :person
|
23
43
|
end
|
24
44
|
end
|
25
45
|
|
@@ -57,11 +77,15 @@ describe Her::Model::Parse do
|
|
57
77
|
Her::API.default_api.connection.adapter :test do |stub|
|
58
78
|
stub.post("/users") { |env| [200, {}, { :user => { :id => 1, :fullname => "Lindsay Fünke" } }.to_json] }
|
59
79
|
stub.get("/users") { |env| [200, {}, [{ :user => { :id => 1, :fullname => "Lindsay Fünke" } }].to_json] }
|
80
|
+
stub.get("/users/admins") { |env| [200, {}, [{ :user => { :id => 1, :fullname => "Lindsay Fünke" } }].to_json] }
|
60
81
|
stub.get("/users/1") { |env| [200, {}, { :user => { :id => 1, :fullname => "Lindsay Fünke" } }.to_json] }
|
61
82
|
stub.put("/users/1") { |env| [200, {}, { :user => { :id => 1, :fullname => "Tobias Fünke Jr." } }.to_json] }
|
62
83
|
end
|
63
84
|
|
64
|
-
spawn_model("Foo::User")
|
85
|
+
spawn_model("Foo::User") do
|
86
|
+
parse_root_in_json true
|
87
|
+
custom_get :admins
|
88
|
+
end
|
65
89
|
end
|
66
90
|
|
67
91
|
it "parse the data from the JSON root element after .create" do
|
@@ -69,6 +93,11 @@ describe Her::Model::Parse do
|
|
69
93
|
@new_user.fullname.should == "Lindsay Fünke"
|
70
94
|
end
|
71
95
|
|
96
|
+
it "parse the data from the JSON root element after an arbitrary HTTP request" do
|
97
|
+
@new_user = Foo::User.admins
|
98
|
+
@new_user.first.fullname.should == "Lindsay Fünke"
|
99
|
+
end
|
100
|
+
|
72
101
|
it "parse the data from the JSON root element after .all" do
|
73
102
|
@users = Foo::User.all
|
74
103
|
@users.first.fullname.should == "Lindsay Fünke"
|
data/spec/model/relation_spec.rb
CHANGED
@@ -204,4 +204,23 @@ describe Her::Model::Relation do
|
|
204
204
|
it("should apply the scope to the request") { Foo::User.all.first.should be_active }
|
205
205
|
end
|
206
206
|
end
|
207
|
+
|
208
|
+
describe :map do
|
209
|
+
before do
|
210
|
+
Her::API.setup :url => "https://api.example.com" do |builder|
|
211
|
+
builder.use Her::Middleware::FirstLevelParseJSON
|
212
|
+
builder.adapter :test do |stub|
|
213
|
+
stub.get("/users") do |env|
|
214
|
+
ok! [{ :id => 1, :fullname => "Tobias Fünke" }, { :id => 2, :fullname => "Lindsay Fünke" }]
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
spawn_model 'Foo::User'
|
220
|
+
end
|
221
|
+
|
222
|
+
it "delegates the method to the fetched collection" do
|
223
|
+
Foo::User.all.map(&:fullname).should == ["Tobias Fünke", "Lindsay Fünke"]
|
224
|
+
end
|
225
|
+
end
|
207
226
|
end
|
@@ -11,7 +11,7 @@ module Her
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def params(env)
|
14
|
-
@params ||= Faraday::Utils.
|
14
|
+
@params ||= Faraday::Utils.parse_nested_query(env[:body]).with_indifferent_access.merge(env[:params])
|
15
15
|
end
|
16
16
|
end
|
17
17
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: her
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.6.
|
4
|
+
version: 0.6.6
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Rémi Prévost
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2013-
|
11
|
+
date: 2013-05-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rake
|