her 0.1.7 → 0.1.8
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +7 -3
- data/README.md +82 -40
- data/Rakefile +1 -1
- data/her.gemspec +2 -2
- data/lib/her.rb +1 -1
- data/lib/her/api.rb +48 -32
- data/lib/her/middleware/default_parse_json.rb +18 -8
- data/lib/her/model.rb +3 -1
- data/lib/her/model/hooks.rb +52 -12
- data/lib/her/model/http.rb +142 -0
- data/lib/her/model/introspection.rb +32 -0
- data/lib/her/model/orm.rb +46 -20
- data/lib/her/model/relationships.rb +51 -16
- data/lib/her/version.rb +1 -1
- data/spec/api_spec.rb +25 -4
- data/spec/middleware/default_parse_json_spec.rb +24 -0
- data/spec/model/hooks_spec.rb +80 -80
- data/spec/model/http_spec.rb +58 -0
- data/spec/model/introspection_spec.rb +27 -0
- data/spec/model/orm_spec.rb +8 -0
- metadata +15 -10
data/LICENSE
CHANGED
@@ -1,3 +1,7 @@
|
|
1
|
-
Copyright 2012 Rémi Prévost
|
2
|
-
|
3
|
-
|
1
|
+
Copyright (c) 2012 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.
|
data/README.md
CHANGED
@@ -54,39 +54,40 @@ User.find(1)
|
|
54
54
|
# PUT https://api.example.com/users/1 with the data and return+update the User object
|
55
55
|
```
|
56
56
|
|
57
|
-
##
|
57
|
+
## Middleware
|
58
|
+
|
59
|
+
Since Her relies on [Faraday](https://github.com/technoweenie/faraday) to send HTTP requests, you can add additional middleware to handle requests and responses.
|
60
|
+
|
61
|
+
### Authentication
|
62
|
+
|
63
|
+
Her doesn’t support any kind of authentication. However, it’s very easy to implement one with a request middleware. Using the `add_middleware` key, we add it to the default list of middleware.
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class MyAuthentication < Faraday::Middleware
|
67
|
+
def call(env)
|
68
|
+
env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
|
69
|
+
@all.call(env)
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
Her::API.setup :base_uri => "https://api.example.com", :add_middleware => [MyAuthentication]
|
74
|
+
```
|
75
|
+
|
76
|
+
Now, each HTTP request made by Her will have the `X-API-Token` header.
|
77
|
+
|
78
|
+
### Parsing data
|
58
79
|
|
59
80
|
By default, Her handles JSON data. It expects the data to be formatted in a certain structure. The default is this:
|
60
81
|
|
61
82
|
```javascript
|
62
83
|
// The response of GET /users/1
|
63
|
-
{
|
64
|
-
"data" : {
|
65
|
-
"id" : 1,
|
66
|
-
"name" : "Tobias Fünke"
|
67
|
-
}
|
68
|
-
}
|
84
|
+
{ "data" : { "id" : 1, "name" : "Tobias Fünke" } }
|
69
85
|
|
70
86
|
// The response of GET /users
|
71
|
-
{
|
72
|
-
"data" : [
|
73
|
-
{
|
74
|
-
"id" : 1,
|
75
|
-
"name" : "Tobias Fünke"
|
76
|
-
},
|
77
|
-
{
|
78
|
-
"id" : 2,
|
79
|
-
"name" : "Lindsay Fünke"
|
80
|
-
}
|
81
|
-
],
|
82
|
-
"metadata" : {
|
83
|
-
"page" : 1,
|
84
|
-
"per_page" : 10
|
85
|
-
}
|
86
|
-
}
|
87
|
+
{ "data" : [{ "id" : 1, "name" : "Tobias Fünke" }] }
|
87
88
|
```
|
88
89
|
|
89
|
-
However, you can define your own parsing method, using a
|
90
|
+
However, you can define your own parsing method, using a response middleware. The middleware is expected to set `env[:body]` to a hash with three keys: `data`, `errors` and `metadata`. The following code enables parsing JSON data and treating the result as first-level properties. Using the `parse_middleware` key, we then replace the default parser.
|
90
91
|
|
91
92
|
```ruby
|
92
93
|
class MyCustomParser < Faraday::Response::Middleware
|
@@ -94,16 +95,11 @@ class MyCustomParser < Faraday::Response::Middleware
|
|
94
95
|
json = JSON.parse(env[:body], :symbolize_names => true)
|
95
96
|
errors = json.delete(:errors) || []
|
96
97
|
metadata = json.delete(:metadata) || []
|
97
|
-
env[:body] = {
|
98
|
-
:data => json,
|
99
|
-
:errors => errors,
|
100
|
-
:metadata => metadata,
|
101
|
-
}
|
98
|
+
env[:body] = { :data => json, :errors => errors, :metadata => metadata }
|
102
99
|
end
|
103
100
|
end
|
104
|
-
Her::API.setup :base_uri => "https://api.example.com", :middleware => [MyCustomParser] + Her::API.default_middleware
|
105
|
-
end
|
106
101
|
|
102
|
+
Her::API.setup :base_uri => "https://api.example.com", :parse_middleware => MyCustomParser
|
107
103
|
# User.find(1) will now expect "https://api.example.com/users/1" to return something like '{ "id": 1, "name": "Tobias Fünke" }'
|
108
104
|
```
|
109
105
|
|
@@ -190,25 +186,69 @@ In the future, adding hooks to all models will be possible, as well as defining
|
|
190
186
|
|
191
187
|
## Custom requests
|
192
188
|
|
193
|
-
You can easily
|
189
|
+
You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
|
190
|
+
|
191
|
+
```ruby
|
192
|
+
class User
|
193
|
+
include Her::Model
|
194
|
+
custom_get :popular, :unpopular
|
195
|
+
custom_post :from_default
|
196
|
+
end
|
197
|
+
|
198
|
+
User.popular # => [#<User id=1>, #<User id=2>]
|
199
|
+
# GET /users/popular
|
200
|
+
|
201
|
+
User.unpopular # => [#<User id=3>, #<User id=4>]
|
202
|
+
# GET /users/unpopular
|
203
|
+
|
204
|
+
User.from_default(:name => "Maeby Fünke") # => #<User id=5>
|
205
|
+
# POST /users/from_default?name=Maeby+Fünke
|
206
|
+
```
|
207
|
+
|
208
|
+
You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
|
209
|
+
|
210
|
+
```ruby
|
211
|
+
class User
|
212
|
+
include Her::Model
|
213
|
+
end
|
214
|
+
|
215
|
+
User.get(:popular) # => [#<User id=1>, #<User id=2>]
|
216
|
+
# GET /users/popular
|
217
|
+
|
218
|
+
User.get(:single_best) # => #<User id=1>
|
219
|
+
# GET /users/single_best
|
220
|
+
```
|
221
|
+
|
222
|
+
Also, `get_collection` (which maps the returned data to a collection of resources), `get_resource` (which maps the returned data to a single resource) or `get_raw` (which yields the parsed data return from the HTTP request) can also be used. Other HTTP methods are supported (`post_raw`, `put_resource`, etc.).
|
194
223
|
|
195
224
|
```ruby
|
196
225
|
class User
|
197
226
|
include Her::Model
|
198
227
|
|
199
228
|
def self.popular
|
200
|
-
get_collection(
|
229
|
+
get_collection(:popular)
|
201
230
|
end
|
202
231
|
|
203
232
|
def self.total
|
204
|
-
get_raw(
|
233
|
+
get_raw(:stats) do |parsed_data|
|
205
234
|
parsed_data[:data][:total_users]
|
206
235
|
end
|
207
236
|
end
|
208
237
|
end
|
209
238
|
|
210
|
-
User.popular
|
211
|
-
User.total
|
239
|
+
User.popular # => [#<User id=1>, #<User id=2>]
|
240
|
+
User.total # => 42
|
241
|
+
```
|
242
|
+
|
243
|
+
You can also use full request paths (with strings instead of symbols).
|
244
|
+
|
245
|
+
```ruby
|
246
|
+
class User
|
247
|
+
include Her::Model
|
248
|
+
end
|
249
|
+
|
250
|
+
User.get("/users/popular") # => [#<User id=1>, #<User id=2>]
|
251
|
+
# GET /users/popular
|
212
252
|
```
|
213
253
|
|
214
254
|
## Multiple APIs
|
@@ -246,15 +286,17 @@ Category.all
|
|
246
286
|
|
247
287
|
## Things to be done
|
248
288
|
|
249
|
-
*
|
250
|
-
* Hooks before save, update, create, destroy, etc.
|
289
|
+
* Add support for collection paths of nested resources (eg. `/users/:user_id/comments` for the `Comment` model)
|
251
290
|
* Better error handling
|
252
|
-
* Better introspection for debug
|
253
291
|
* Better documentation
|
254
292
|
|
255
293
|
## Contributors
|
256
294
|
|
257
|
-
Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues)
|
295
|
+
Feel free to contribute and submit issues/pull requests [on GitHub](https://github.com/remiprev/her/issues) like these fine folks did:
|
296
|
+
|
297
|
+
* [@jfcixmedia](https://github.com/jfcixmedia)
|
298
|
+
* [@EtienneLem](https://github.com/EtienneLem)
|
299
|
+
* [@rafaelss](https://github.com/rafaelss)
|
258
300
|
|
259
301
|
Take a look at the `spec` folder before you do, and make sure `bundle exec rake spec` passes after your modifications :)
|
260
302
|
|
data/Rakefile
CHANGED
data/her.gemspec
CHANGED
@@ -19,11 +19,11 @@ Gem::Specification.new do |s|
|
|
19
19
|
s.add_development_dependency "rake"
|
20
20
|
s.add_development_dependency "rspec"
|
21
21
|
s.add_development_dependency "yard"
|
22
|
-
s.add_development_dependency "
|
22
|
+
s.add_development_dependency "redcarpet", "1.17.2"
|
23
23
|
s.add_development_dependency "mocha"
|
24
24
|
s.add_development_dependency "fakeweb"
|
25
25
|
|
26
26
|
s.add_runtime_dependency "activesupport"
|
27
27
|
s.add_runtime_dependency "faraday"
|
28
|
-
s.add_runtime_dependency "
|
28
|
+
s.add_runtime_dependency "multi_json"
|
29
29
|
end
|
data/lib/her.rb
CHANGED
data/lib/her/api.rb
CHANGED
@@ -1,49 +1,56 @@
|
|
1
1
|
module Her
|
2
2
|
# This class is where all HTTP requests are made. Before using Her, you must configure it
|
3
3
|
# so it knows where to make those requests. In Rails, this is usually done in `config/initializers/her.rb`:
|
4
|
-
#
|
5
|
-
# @example
|
6
|
-
# $my_api = Her::API.new
|
7
|
-
# $my_api.setup :base_uri => "https://api.example.com"
|
8
4
|
class API
|
9
5
|
# @private
|
10
6
|
attr_reader :base_uri, :middleware
|
11
7
|
|
12
|
-
# Setup a default API connection
|
8
|
+
# Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
|
13
9
|
def self.setup(attrs={}) # {{{
|
14
10
|
@@default_api = new
|
15
11
|
@@default_api.setup(attrs)
|
16
12
|
end # }}}
|
17
13
|
|
18
|
-
#
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
# @
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
#
|
14
|
+
# Setup the API connection.
|
15
|
+
#
|
16
|
+
# @param [Hash] attrs the options to create a message with
|
17
|
+
# @option attrs [String] :base_uri The main HTTP API root (eg. `https://api.example.com`)
|
18
|
+
# @option attrs [Array, Class] :middleware A list of the only middleware Her will use
|
19
|
+
# @option attrs [Array, Class] :add_middleware A list of middleware to add to Her’s default middleware
|
20
|
+
# @option attrs [Class] :parse_middleware A middleware that will replace {Her::Middleware::DefaultParseJSON} to parse the received JSON
|
21
|
+
#
|
22
|
+
# @example Setting up the default API connection
|
23
|
+
# Her::API.setup :base_uri => "https://api.example"
|
24
|
+
#
|
25
|
+
# @example A custom middleware added to the default list
|
26
|
+
# class MyAuthentication < Faraday::Middleware
|
27
|
+
# def call(env)
|
28
|
+
# env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
|
29
|
+
# @all.call(env)
|
30
|
+
# end
|
31
|
+
# end
|
32
|
+
# Her::API.setup :base_uri => "https://api.example.com", :add_middleware => [MyAuthentication]
|
29
33
|
#
|
30
|
-
# @example
|
31
|
-
#
|
32
|
-
#
|
33
|
-
#
|
34
|
-
#
|
35
|
-
#
|
36
|
-
#
|
37
|
-
# :errors => json[:errors] || [],
|
38
|
-
# :metadata => json[:metadata] || {},
|
39
|
-
# }
|
40
|
-
# end
|
34
|
+
# @example A custom parse middleware
|
35
|
+
# class MyCustomParser < Faraday::Response::Middleware
|
36
|
+
# def on_complete(env)
|
37
|
+
# json = JSON.parse(env[:body], :symbolize_names => true)
|
38
|
+
# errors = json.delete(:errors) || []
|
39
|
+
# metadata = json.delete(:metadata) || []
|
40
|
+
# env[:body] = { :data => json, :errors => errors, :metadata => metadata }
|
41
41
|
# end
|
42
42
|
# end
|
43
|
-
# Her::API.setup :
|
43
|
+
# Her::API.setup :base_uri => "https://api.example.com", :parse_middleware => MyCustomParser
|
44
44
|
def setup(attrs={}) # {{{
|
45
45
|
@base_uri = attrs[:base_uri]
|
46
|
-
|
46
|
+
@middleware = Her::API.default_middleware
|
47
|
+
|
48
|
+
@middleware = [attrs[:middleware]] if attrs[:middleware]
|
49
|
+
@middleware = [attrs[:add_middleware]] + @middleware if attrs[:add_middleware]
|
50
|
+
@middleware = [attrs[:parse_middleware]] + @middleware.reject { |item| item == Her::Middleware::DefaultParseJSON } if attrs[:parse_middleware]
|
51
|
+
|
52
|
+
@middleware.flatten!
|
53
|
+
middleware = @middleware
|
47
54
|
@connection = Faraday.new(:url => @base_uri) do |builder|
|
48
55
|
middleware.each { |m| builder.use(m) }
|
49
56
|
end
|
@@ -53,9 +60,7 @@ module Her
|
|
53
60
|
# expected to return a hash with three keys: a main data Hash, an errors Array
|
54
61
|
# and a metadata Hash.
|
55
62
|
#
|
56
|
-
# @
|
57
|
-
|
58
|
-
# Make an HTTP request to the API
|
63
|
+
# @private
|
59
64
|
def request(attrs={}) # {{{
|
60
65
|
method = attrs.delete(:_method)
|
61
66
|
path = attrs.delete(:_path)
|
@@ -71,5 +76,16 @@ module Her
|
|
71
76
|
end
|
72
77
|
response.env[:body]
|
73
78
|
end # }}}
|
79
|
+
|
80
|
+
private
|
81
|
+
# @private
|
82
|
+
def self.default_api(attrs={}) # {{{
|
83
|
+
defined?(@@default_api) ? @@default_api : nil
|
84
|
+
end # }}}
|
85
|
+
|
86
|
+
# @private
|
87
|
+
def self.default_middleware # {{{
|
88
|
+
[Her::Middleware::DefaultParseJSON, Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]
|
89
|
+
end # }}}
|
74
90
|
end
|
75
91
|
end
|
@@ -1,18 +1,28 @@
|
|
1
1
|
module Her
|
2
2
|
module Middleware
|
3
|
+
# This is the default middleware used to parse JSON responses. It returns
|
4
|
+
# a Hash with three elements: `data`, `errors` and `metadata`.
|
3
5
|
class DefaultParseJSON < Faraday::Response::Middleware
|
4
|
-
|
5
|
-
|
6
|
-
|
6
|
+
# Parse the response body
|
7
|
+
#
|
8
|
+
# @param [String] body The response body
|
9
|
+
# @return [Mixed] the parsed response
|
10
|
+
def parse(body) # {{{
|
11
|
+
json = MultiJson.load(body, :symbolize_keys => true)
|
12
|
+
{
|
7
13
|
:data => json[:data],
|
8
14
|
:errors => json[:errors],
|
9
|
-
:metadata => json[:metadata]
|
15
|
+
:metadata => json[:metadata]
|
10
16
|
}
|
11
|
-
end
|
17
|
+
end # }}}
|
12
18
|
|
13
|
-
|
14
|
-
|
15
|
-
|
19
|
+
# This method is triggered when the response has been received. It modifies
|
20
|
+
# the value of `env[:body]`.
|
21
|
+
#
|
22
|
+
# @param [Hash] env The response environment
|
23
|
+
def on_complete(env) # {{{
|
24
|
+
env[:body] = parse(env[:body])
|
25
|
+
end # }}}
|
16
26
|
end
|
17
27
|
end
|
18
28
|
end
|
data/lib/her/model.rb
CHANGED
@@ -5,21 +5,23 @@ module Her
|
|
5
5
|
# @example
|
6
6
|
# class User
|
7
7
|
# include Her::Model
|
8
|
-
# uses_api $api
|
9
8
|
# end
|
10
9
|
#
|
11
10
|
# @user = User.new(:name => "Rémi")
|
11
|
+
# @user.save
|
12
12
|
module Model
|
13
13
|
autoload :Base, "her/model/base"
|
14
14
|
autoload :HTTP, "her/model/http"
|
15
15
|
autoload :ORM, "her/model/orm"
|
16
16
|
autoload :Relationships, "her/model/relationships"
|
17
17
|
autoload :Hooks, "her/model/hooks"
|
18
|
+
autoload :Introspection, "her/model/introspection"
|
18
19
|
|
19
20
|
extend ActiveSupport::Concern
|
20
21
|
|
21
22
|
# Instance methods
|
22
23
|
include Her::Model::ORM
|
24
|
+
include Her::Model::Introspection
|
23
25
|
|
24
26
|
# Class methods
|
25
27
|
included do
|
data/lib/her/model/hooks.rb
CHANGED
@@ -1,7 +1,57 @@
|
|
1
1
|
module Her
|
2
2
|
module Model
|
3
|
+
# Her supports hooks/callbacks that are triggered whenever resources are created, updated or destroyed.
|
4
|
+
#
|
5
|
+
# @example Defining a hook with a block
|
6
|
+
# class User
|
7
|
+
# include Her::Model
|
8
|
+
# before_save { |resource| resource.internal_id = 42 }
|
9
|
+
# end
|
10
|
+
#
|
11
|
+
# @example Defining a hook with a method name
|
12
|
+
# class User
|
13
|
+
# include Her::Model
|
14
|
+
# before_save :set_internal_id
|
15
|
+
#
|
16
|
+
# private
|
17
|
+
# def set_internal_id
|
18
|
+
# self.internal_id = 42
|
19
|
+
# end
|
20
|
+
# end
|
3
21
|
module Hooks
|
4
|
-
#
|
22
|
+
# Add a *before save* callback. Triggered before a resource is created or updated.
|
23
|
+
# @param [Symbol, &block] method A method or a block to be called
|
24
|
+
def before_save(method=nil, &block); set_hook(:before, :save, method || block); end
|
25
|
+
|
26
|
+
# Add a *before create* callback. Triggered before a resource is created.
|
27
|
+
# @param [Symbol, &block] method A method or a block to be called
|
28
|
+
def before_create(method=nil, &block); set_hook(:before, :create, method || block); end
|
29
|
+
|
30
|
+
# Add a *before update* callback. Triggered before a resource is updated.
|
31
|
+
# @param [Symbol, &block] method A method or a block to be called
|
32
|
+
def before_update(method=nil, &block); set_hook(:before, :update, method || block); end
|
33
|
+
|
34
|
+
# Add a *before destroy* callback. Triggered before a resource is destroyed.
|
35
|
+
# @param [Symbol, &block] method A method or a block to be called
|
36
|
+
def before_destroy(method=nil, &block); set_hook(:before, :destroy, method || block); end
|
37
|
+
|
38
|
+
# Add a *after save* callback. Triggered after a resource is created or updated.
|
39
|
+
# @param [Symbol, &block] method A method or a block to be called
|
40
|
+
def after_save(method=nil, &block); set_hook(:after, :save, method || block); end
|
41
|
+
|
42
|
+
# Add a *after create* callback. Triggered after a resource is created.
|
43
|
+
# @param [Symbol, &block] method A method or a block to be called
|
44
|
+
def after_create(method=nil, &block); set_hook(:after, :create, method || block); end
|
45
|
+
|
46
|
+
# Add a *after update* callback. Triggered after a resource is updated.
|
47
|
+
# @param [Symbol, &block] method A method or a block to be called
|
48
|
+
def after_update(method=nil, &block); set_hook(:after, :update, method || block); end
|
49
|
+
|
50
|
+
# Add a *after destroy* callback. Triggered after a resource is destroyed.
|
51
|
+
# @param [Symbol, &block] method A method or a block to be called
|
52
|
+
def after_destroy(method=nil, &block); set_hook(:after, :destroy, method || block); end
|
53
|
+
|
54
|
+
private
|
5
55
|
# @private
|
6
56
|
def hooks # {{{
|
7
57
|
@her_hooks
|
@@ -25,16 +75,6 @@ module Her
|
|
25
75
|
end
|
26
76
|
end
|
27
77
|
end # }}}
|
28
|
-
|
29
|
-
def before_save(method=nil, &block); set_hook(:before, :save, method || block); end
|
30
|
-
def before_create(method=nil, &block); set_hook(:before, :create, method || block); end
|
31
|
-
def before_update(method=nil, &block); set_hook(:before, :update, method || block); end
|
32
|
-
def before_destroy(method=nil, &block); set_hook(:before, :destroy, method || block); end
|
33
|
-
|
34
|
-
def after_save(method=nil, &block); set_hook(:after, :save, method || block); end
|
35
|
-
def after_create(method=nil, &block); set_hook(:after, :create, method || block); end
|
36
|
-
def after_update(method=nil, &block); set_hook(:after, :update, method || block); end
|
37
|
-
def after_destroy(method=nil, &block); set_hook(:after, :destroy, method || block); end
|
38
|
-
end # }}}
|
78
|
+
end
|
39
79
|
end
|
40
80
|
end
|