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 CHANGED
@@ -1,3 +1,7 @@
1
- Copyright 2012 Rémi Prévost.
2
- You may use this work without restrictions, as long as this notice is included.
3
- The work is provided "as is" without warranty of any kind, neither express nor implied.
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
- ## Parsing data
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 Faraday response middleware. The middleware is expected to return a hash with three keys: `data`, `errors` and `metadata`. The following code enables parsing JSON data and treating this data as first-level properties:
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 add custom methods for your models. You can either use `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). Other HTTP methods are supported (`post_raw`, `put_resource`, etc.)
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("/users/popular")
229
+ get_collection(:popular)
201
230
  end
202
231
 
203
232
  def self.total
204
- get_raw("/users/stats") do |parsed_data|
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 # => [#<User id=1>, #<User id=2>]
211
- User.total # => 42
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
- * Support for Faraday middleware to handle caching, alternative formats, etc.
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
@@ -18,7 +18,7 @@ YARD::Rake::YardocTask.new do |task| # {{{
18
18
  "-o", File.expand_path("../doc", __FILE__),
19
19
  "--readme=README.md",
20
20
  "--markup=markdown",
21
- "--markup-provider=maruku",
21
+ "--markup-provider=redcarpet",
22
22
  "--no-private",
23
23
  "--no-cache",
24
24
  "--protected",
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 "maruku"
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 "json"
28
+ s.add_runtime_dependency "multi_json"
29
29
  end
data/lib/her.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  require "her/version"
2
- require "json"
2
+ require "multi_json"
3
3
  require "faraday"
4
4
  require "active_support"
5
5
  require "active_support/inflector"
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
- # @private
19
- def self.default_api(attrs={}) # {{{
20
- defined?(@@default_api) ? @@default_api : nil
21
- end # }}}
22
-
23
- # @private
24
- def self.default_middleware # {{{
25
- [Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]
26
- end # }}}
27
-
28
- # Setup the API connection
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
- # module MyAPI
32
- # class ParseResponse
33
- # def on_complete(env)
34
- # json = JSON.parse(env[:body], :symbolize_names => true)
35
- # {
36
- # :data => json,
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 :base_url => "https://api.example.com", :middleware => [MyAPI::ParseResponse, Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]
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
- middleware = @middleware = attrs[:middleware] || [Her::Middleware::DefaultParseJSON] + Her::API.default_middleware
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
- # @example
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
- def parse(body)
5
- json = JSON.parse(body, :symbolize_names => true)
6
- return {
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
- def on_complete(env)
14
- env[:body] = parse env[:body]
15
- end
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
@@ -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
- # Return hooks
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