her 0.3.6 → 0.3.7

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.
data/.gitignore CHANGED
@@ -5,3 +5,4 @@ pkg/*
5
5
  .yardoc
6
6
  doc
7
7
  rake
8
+ tmp
@@ -0,0 +1,26 @@
1
+ # How to contribute
2
+
3
+ _(This file is heavily based on [factory\_girl\_rails](https://github.com/thoughtbot/factory_girl_rails/blob/master/CONTRIBUTING.md)’s Contribution Guide)_
4
+
5
+ We love pull requests. Here’s a quick guide:
6
+
7
+ * Fork the repository.
8
+ * Run `rake spec` (to make sure you start with a clean slate).
9
+ * Implement your feature or fix.
10
+ * Add examples that describe it (in the `spec` directory). Only refactoring and documentation changes require no new tests. If you are adding functionality or fixing a bug, we need examples!
11
+ * Make sure `rake spec` passes after your modifications.
12
+ * Commit (bonus points for doing it in a `feature-*` branch).
13
+ * Push to your fork and send your pull request!
14
+
15
+ If we have not replied to your pull request in three or four days, do not hesitate to post another comment in it — yes, we can be lazy sometimes.
16
+
17
+ ## Syntax Guide
18
+
19
+ Do not hesitate to submit patches that fix syntax issues. Some may have slipped under our nose.
20
+
21
+ * Two spaces, no tabs (but you already knew that, right?).
22
+ * No trailing whitespace. Blank lines should not have any space. There are few things we **hate** more than trailing whitespace. Seriously.
23
+ * `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
24
+ * `[:foo, :bar]` and not `[ :foo, :bar ]`, `{ :foo => :bar }` and not `{:foo => :bar}`
25
+ * `a = b` and not `a=b`.
26
+ * Follow the conventions you see used in the source already.
@@ -0,0 +1,296 @@
1
+ # Features
2
+
3
+ ## Methods
4
+
5
+ ```ruby
6
+ class User
7
+ include Her::Model
8
+ end
9
+
10
+ # Update a fetched resource
11
+ user = User.find(1)
12
+ user.fullname = "Lindsay Fünke"
13
+ # OR user.assign_attributes :fullname => "Lindsay Fünke"
14
+ user.save
15
+
16
+ # Update a resource without fetching it
17
+ User.save_existing(1, :fullname => "Lindsay Fünke")
18
+
19
+ # Destroy a fetched resource
20
+ user = User.find(1)
21
+ user.destroy
22
+
23
+ # Destroy a resource without fetching it
24
+ User.destroy_existing(1)
25
+
26
+ # Fetching a collection of resources
27
+ User.all
28
+
29
+ # Create a new resource
30
+ User.create(:fullname => "Maeby Fünke")
31
+
32
+ # Save a new resource
33
+ user = User.new(:fullname => "Maeby Fünke")
34
+ user.save
35
+ ```
36
+
37
+ ## Relationships
38
+
39
+ You can define `has_many`, `has_one` and `belongs_to` relationships in your models. The relationship data is handled in two different ways.
40
+
41
+ 1. If Her finds relationship data when parsing a resource, that data will be used to create the associated model objects on the resource.
42
+ 2. If no relationship data was included when parsing a resource, calling a method with the same name as the relationship will fetch the data (providing there’s an HTTP request available for it in the API).
43
+
44
+ For example:
45
+
46
+ ```ruby
47
+ class User
48
+ include Her::Model
49
+ has_many :comments
50
+ has_one :role
51
+ belongs_to :organization
52
+ end
53
+
54
+ class Comment
55
+ include Her::Model
56
+ end
57
+
58
+ class Role
59
+ include Her::Model
60
+ end
61
+
62
+ class Organization
63
+ include Her::Model
64
+ end
65
+ ```
66
+
67
+ If there’s relationship data in the resource, no extra HTTP request is made when calling the `#comments` method and an array of resources is returned:
68
+
69
+ ```ruby
70
+ @user = User.find(1)
71
+ # {
72
+ # :data => {
73
+ # :id => 1,
74
+ # :name => "George Michael Bluth",
75
+ # :comments => [
76
+ # { :id => 1, :text => "Foo" },
77
+ # { :id => 2, :text => "Bar" }
78
+ # ],
79
+ # :role => { :id => 1, :name => "Admin" },
80
+ # :organization => { :id => 2, :name => "Bluth Company" }
81
+ # }
82
+ # }
83
+ @user.comments
84
+ # [#<Comment id=1 text="Foo">, #<Comment id=2 text="Bar">]
85
+ @user.role
86
+ # #<Role id=1 name="Admin">
87
+ @user.organization
88
+ # #<Organization id=2 name="Bluth Company">
89
+ ```
90
+
91
+ If there’s no relationship data in the resource, Her makes a HTTP request to retrieve the data.
92
+
93
+ ```ruby
94
+ @user = User.find(1)
95
+ # { :data => { :id => 1, :name => "George Michael Bluth", :organization_id => 2 }}
96
+
97
+ # has_many relationship:
98
+ @user.comments
99
+ # GET /users/1/comments
100
+ # [#<Comment id=1>, #<Comment id=2>]
101
+
102
+ # has_one relationship:
103
+ @user.role
104
+ # GET /users/1/role
105
+ # #<Role id=1>
106
+
107
+ # belongs_to relationship:
108
+ @user.organization
109
+ # (the organization id comes from :organization_id, by default)
110
+ # GET /organizations/2
111
+ # #<Organization id=2>
112
+ ```
113
+
114
+ Subsequent calls to `#comments`, `#role` and `#organization` will not trigger extra HTTP requests and will return the cached objects.
115
+
116
+ ## Hooks
117
+
118
+ You can add *before* and *after* hooks to your models that are triggered on specific actions (`save`, `update`, `create`, `destroy`):
119
+
120
+ ```ruby
121
+ class User
122
+ include Her::Model
123
+ before_save :set_internal_id
124
+
125
+ def set_internal_id
126
+ self.internal_id = 42 # Will be passed in the HTTP request
127
+ end
128
+ end
129
+
130
+ @user = User.create(:fullname => "Tobias Fünke")
131
+ # POST /users&fullname=Tobias+Fünke&internal_id=42
132
+ ```
133
+
134
+ ## Custom requests
135
+
136
+ You can easily define custom requests for your models using `custom_get`, `custom_post`, etc.
137
+
138
+ ```ruby
139
+ class User
140
+ include Her::Model
141
+ custom_get :popular, :unpopular
142
+ custom_post :from_default
143
+ end
144
+
145
+ User.popular
146
+ # GET /users/popular
147
+ # [#<User id=1>, #<User id=2>]
148
+
149
+ User.unpopular
150
+ # GET /users/unpopular
151
+ # [#<User id=3>, #<User id=4>]
152
+
153
+ User.from_default(:name => "Maeby Fünke")
154
+ # POST /users/from_default?name=Maeby+Fünke
155
+ # #<User id=5 name="Maeby Fünke">
156
+ ```
157
+
158
+ You can also use `get`, `post`, `put` or `delete` (which maps the returned data to either a collection or a resource).
159
+
160
+ ```ruby
161
+ class User
162
+ include Her::Model
163
+ end
164
+
165
+ User.get(:popular)
166
+ # GET /users/popular
167
+ # [#<User id=1>, #<User id=2>]
168
+
169
+ User.get(:single_best)
170
+ # GET /users/single_best
171
+ # #<User id=1>
172
+ ```
173
+
174
+ 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.).
175
+
176
+ ```ruby
177
+ class User
178
+ include Her::Model
179
+
180
+ def self.popular
181
+ get_collection(:popular)
182
+ end
183
+
184
+ def self.total
185
+ get_raw(:stats) do |parsed_data|
186
+ parsed_data[:data][:total_users]
187
+ end
188
+ end
189
+ end
190
+
191
+ User.popular
192
+ # GET /users/popular
193
+ # [#<User id=1>, #<User id=2>]
194
+ User.total
195
+ # GET /users/stats
196
+ # => 42
197
+ ```
198
+
199
+ You can also use full request paths (with strings instead of symbols).
200
+
201
+ ```ruby
202
+ class User
203
+ include Her::Model
204
+ end
205
+
206
+ User.get("/users/popular")
207
+ # GET /users/popular
208
+ # [#<User id=1>, #<User id=2>]
209
+ ```
210
+
211
+ ## Custom paths
212
+
213
+ You can define custom HTTP paths for your models:
214
+
215
+ ```ruby
216
+ class User
217
+ include Her::Model
218
+ collection_path "/hello_users/:id"
219
+ end
220
+
221
+ @user = User.find(1)
222
+ # GET /hello_users/1
223
+ ```
224
+
225
+ You can also include custom variables in your paths:
226
+
227
+ ```ruby
228
+ class User
229
+ include Her::Model
230
+ collection_path "/organizations/:organization_id/users"
231
+ end
232
+
233
+ @user = User.find(1, :_organization_id => 2)
234
+ # GET /organizations/2/users/1
235
+
236
+ @user = User.all(:_organization_id => 2)
237
+ # GET /organizations/2/users
238
+
239
+ @user = User.new(:fullname => "Tobias Fünke", :organization_id => 2)
240
+ @user.save
241
+ # POST /organizations/2/users
242
+ ```
243
+
244
+ ## Multiple APIs
245
+
246
+ It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
247
+
248
+ ```ruby
249
+ # config/initializers/her.rb
250
+ $my_api = Her::API.new
251
+ $my_api.setup :url => "https://my_api.example.com" do |connection|
252
+ connection.use Faraday::Request::UrlEncoded
253
+ connection.use Her::Middleware::DefaultParseJSON
254
+ connection.use Faraday::Adapter::NetHttp
255
+ end
256
+
257
+ $other_api = Her::API.new
258
+ $other_api.setup :url => "https://other_api.example.com" do |connection|
259
+ connection.use Faraday::Request::UrlEncoded
260
+ connection.use Her::Middleware::DefaultParseJSON
261
+ connection.use Faraday::Adapter::NetHttp
262
+ end
263
+ ```
264
+
265
+ You can then define which API a model will use:
266
+
267
+ ```ruby
268
+ class User
269
+ include Her::Model
270
+ uses_api $my_api
271
+ end
272
+
273
+ class Category
274
+ include Her::Model
275
+ uses_api $other_api
276
+ end
277
+
278
+ User.all
279
+ # GET https://my_api.example.com/users
280
+
281
+ Category.all
282
+ # GET https://other_api.example.com/categories
283
+ ```
284
+
285
+ ## SSL
286
+
287
+ When initializing `Her::API`, you can pass any parameter supported by `Faraday.new`. So [to use HTTPS](https://github.com/technoweenie/faraday/wiki/Setting-up-SSL-certificates), you can use Faraday’s `:ssl` option.
288
+
289
+ ```ruby
290
+ ssl_options = { :ca_path => "/usr/lib/ssl/certs" }
291
+ Her::API.setup :url => "https://api.example.com", :ssl => ssl_options do |connection|
292
+ connection.use Faraday::Request::UrlEncoded
293
+ connection.use Her::Middleware::DefaultParseJSON
294
+ connection.use Faraday::Adapter::NetHttp
295
+ end
296
+ ```
@@ -0,0 +1,183 @@
1
+ # Middleware
2
+
3
+ Since Her relies on [Faraday](https://github.com/technoweenie/faraday) to send HTTP requests, you can choose the middleware used to handle requests and responses. Using the block in the `setup` call, you have access to Faraday’s `connection` object and are able to customize the middleware stack used on each request and response.
4
+
5
+ ## Authentication
6
+
7
+ Her doesn’t support authentication by default. However, it’s easy to implement one with request middleware. Using the `connection` block, we can add it to the middleware stack.
8
+
9
+ For example, to add a API token header to your requests in a Rails application, you would do something like this:
10
+
11
+ ```ruby
12
+ # app/controllers/application_controller.rb
13
+ class ApplicationController < ActionController::Base
14
+ around_filter :do_with_authenticated_user
15
+
16
+ def do_with_authenticated_user
17
+ Thread.current[:my_api_token] = session[:my_api_token]
18
+ begin
19
+ yield
20
+ ensure
21
+ Thread.current[:my_access_token] = nil
22
+ end
23
+ end
24
+ end
25
+
26
+ # lib/my_token_authentication.rb
27
+ class MyTokenAuthentication < Faraday::Middleware
28
+ def initialize(app, options={})
29
+ @app = app
30
+ end
31
+
32
+ def call(env)
33
+ env[:request_headers]["X-API-Token"] = Thread.current[:my_api_token] if Thread.current[:my_api_token].present?
34
+ @app.call(env)
35
+ end
36
+ end
37
+
38
+ # config/initializers/her.rb
39
+ require "lib/my_token_authentication"
40
+
41
+ Her::API.setup :url => "https://api.example.com" do |connection|
42
+ connection.use MyTokenAuthentication
43
+ connection.use Faraday::Request::UrlEncoded
44
+ connection.use Her::Middleware::DefaultParseJSON
45
+ connection.use Faraday::Adapter::NetHttp
46
+ end
47
+ ```
48
+
49
+ Now, each HTTP request made by Her will have the `X-API-Token` header.
50
+
51
+ ## Parsing JSON data
52
+
53
+ By default, Her handles JSON data. It expects the resource/collection data to be returned at the first level.
54
+
55
+ ```javascript
56
+ // The response of GET /users/1
57
+ { "id" : 1, "name" : "Tobias Fünke" }
58
+
59
+ // The response of GET /users
60
+ [{ "id" : 1, "name" : "Tobias Fünke" }]
61
+ ```
62
+
63
+ However, you can define your own parsing method using a response middleware. The middleware should set `env[:body]` to a hash with three keys: `data`, `errors` and `metadata`. The following code uses a custom middleware to parse the JSON data:
64
+
65
+ ```ruby
66
+ # Expects responses like:
67
+ #
68
+ # {
69
+ # "result": {
70
+ # "id": 1,
71
+ # "name": "Tobias Fünke"
72
+ # },
73
+ # "errors" => []
74
+ # }
75
+ #
76
+ class MyCustomParser < Faraday::Response::Middleware
77
+ def on_complete(env)
78
+ json = MultiJson.load(env[:body], :symbolize_keys => true)
79
+ env[:body] = {
80
+ :data => json[:result],
81
+ :errors => json[:errors],
82
+ :metadata => json[:metadata]
83
+ }
84
+ end
85
+ end
86
+
87
+ Her::API.setup :url => "https://api.example.com" do |connection|
88
+ connection.use Faraday::Request::UrlEncoded
89
+ connection.use MyCustomParser
90
+ connection.use Faraday::Adapter::NetHttp
91
+ end
92
+ ```
93
+
94
+ ## OAuth
95
+
96
+ Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
97
+
98
+ In your Gemfile:
99
+
100
+ ```ruby
101
+ gem "her"
102
+ gem "faraday_middleware"
103
+ gem "simple_oauth"
104
+ ```
105
+
106
+ In your Ruby code:
107
+
108
+ ```ruby
109
+ # Create an application on `https://dev.twitter.com/apps` to set these values
110
+ TWITTER_CREDENTIALS = {
111
+ :consumer_key => "",
112
+ :consumer_secret => "",
113
+ :token => "",
114
+ :token_secret => ""
115
+ }
116
+
117
+ Her::API.setup :url => "https://api.twitter.com/1/" do |connection|
118
+ connection.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
119
+ connection.use Faraday::Request::UrlEncoded
120
+ connection.use Her::Middleware::DefaultParseJSON
121
+ connection.use Faraday::Adapter::NetHttp
122
+ end
123
+
124
+ class Tweet
125
+ include Her::Model
126
+ end
127
+
128
+ @tweets = Tweet.get("/statuses/home_timeline.json")
129
+ ```
130
+
131
+ See the *Authentication* middleware section for an example of how to pass different credentials based on the current user.
132
+
133
+ ## Caching
134
+
135
+ Again, using the `faraday_middleware` makes it very easy to cache requests and responses:
136
+
137
+ In your Gemfile:
138
+
139
+ ```ruby
140
+ gem "her"
141
+ gem "faraday_middleware"
142
+ ```
143
+
144
+ In your Ruby code:
145
+
146
+ ```ruby
147
+ class MyCache < Hash
148
+ def read(key)
149
+ if cached = self[key]
150
+ Marshal.load(cached)
151
+ end
152
+ end
153
+
154
+ def write(key, data)
155
+ self[key] = Marshal.dump(data)
156
+ end
157
+
158
+ def fetch(key)
159
+ read(key) || yield.tap { |data| write(key, data) }
160
+ end
161
+ end
162
+
163
+ # A cache system must respond to `#write`, `#read` and `#fetch`.
164
+ # We should be probably using something like Memcached here, not a global object
165
+ $cache = MyCache.new
166
+
167
+ Her::API.setup :url => "https://api.example.com" do |connection|
168
+ connection.use Faraday::Request::UrlEncoded
169
+ connection.use FaradayMiddleware::Caching, $cache
170
+ connection.use Her::Middleware::DefaultParseJSON
171
+ connection.use Faraday::Adapter::NetHttp
172
+ end
173
+
174
+ class User
175
+ include Her::Model
176
+ end
177
+
178
+ @user = User.find(1)
179
+ # GET /users/1
180
+
181
+ @user = User.find(1)
182
+ # This request will be fetched from the cache
183
+ ```