her 0.3.6 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
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
+ ```