her 0.6.5 → 0.6.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|