her 0.2 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Guardfile +7 -0
- data/README.md +111 -9
- data/examples/twitter-oauth/Gemfile +13 -0
- data/examples/twitter-oauth/app.rb +42 -0
- data/examples/twitter-oauth/config.ru +5 -0
- data/examples/twitter-oauth/views/index.haml +9 -0
- data/examples/twitter-search/Gemfile +11 -0
- data/examples/twitter-search/app.rb +30 -0
- data/examples/twitter-search/config.ru +5 -0
- data/examples/twitter-search/views/index.haml +9 -0
- data/her.gemspec +14 -10
- data/lib/her.rb +1 -0
- data/lib/her/api.rb +27 -7
- data/lib/her/errors.rb +5 -0
- data/lib/her/middleware.rb +2 -0
- data/lib/her/model.rb +5 -2
- data/lib/her/model/http.rb +20 -44
- data/lib/her/model/introspection.rb +1 -3
- data/lib/her/model/orm.rb +8 -9
- data/lib/her/model/paths.rb +59 -0
- data/lib/her/model/relationships.rb +3 -6
- data/lib/her/version.rb +1 -1
- data/spec/api_spec.rb +15 -45
- data/spec/model/hooks_spec.rb +1 -2
- data/spec/model/introspection_spec.rb +2 -2
- data/spec/model/orm_spec.rb +5 -4
- data/spec/model/paths_spec.rb +131 -0
- data/spec/model/relationships_spec.rb +6 -4
- data/spec/spec_helper.rb +3 -2
- metadata +115 -38
data/Guardfile
ADDED
data/README.md
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# Her
|
2
2
|
|
3
|
-
[![Build Status](https://secure.travis-ci.org/remiprev/her.png)](http://travis-ci.org/remiprev/her)
|
4
|
-
|
5
3
|
Her is an ORM (Object Relational Mapper) that maps REST resources to Ruby objects. It is designed to build applications that are powered by a RESTful API and no database.
|
6
4
|
|
5
|
+
[![Build Status](https://secure.travis-ci.org/remiprev/her.png)](http://travis-ci.org/remiprev/her)
|
6
|
+
|
7
7
|
## Installation
|
8
8
|
|
9
9
|
In your Gemfile, add:
|
@@ -60,17 +60,19 @@ Since Her relies on [Faraday](https://github.com/technoweenie/faraday) to send H
|
|
60
60
|
|
61
61
|
### Authentication
|
62
62
|
|
63
|
-
Her doesn’t support any kind of authentication. However, it’s very easy to implement one with a request middleware. Using the
|
63
|
+
Her doesn’t support any kind of authentication. However, it’s very easy to implement one with a request middleware. Using the builder block, we add it to the default list of middleware.
|
64
64
|
|
65
65
|
```ruby
|
66
66
|
class MyAuthentication < Faraday::Middleware
|
67
67
|
def call(env)
|
68
68
|
env[:request_headers]["X-API-Token"] = "bb2b2dd75413d32c1ac421d39e95b978d1819ff611f68fc2fdd5c8b9c7331192"
|
69
|
-
@
|
69
|
+
@app.call(env)
|
70
70
|
end
|
71
71
|
end
|
72
72
|
|
73
|
-
Her::API.setup :base_uri => "https://api.example.com"
|
73
|
+
Her::API.setup :base_uri => "https://api.example.com" do |builder|
|
74
|
+
builder.use MyAuthentication
|
75
|
+
end
|
74
76
|
```
|
75
77
|
|
76
78
|
Now, each HTTP request made by Her will have the `X-API-Token` header.
|
@@ -89,12 +91,12 @@ By default, Her handles JSON data. It expects the resource/collection data to be
|
|
89
91
|
[{ "id" : 1, "name" : "Tobias Fünke" }]
|
90
92
|
```
|
91
93
|
|
92
|
-
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
|
94
|
+
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 builder block, we then replace the default parser.
|
93
95
|
|
94
96
|
```ruby
|
95
97
|
class MyCustomParser < Faraday::Response::Middleware
|
96
98
|
def on_complete(env)
|
97
|
-
json = MultiJson.load(body, :symbolize_keys => true)
|
99
|
+
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
98
100
|
env[:body] = {
|
99
101
|
:data => json[:data],
|
100
102
|
:errors => json[:errors],
|
@@ -103,10 +105,82 @@ class MyCustomParser < Faraday::Response::Middleware
|
|
103
105
|
end
|
104
106
|
end
|
105
107
|
|
106
|
-
Her::API.setup :base_uri => "https://api.example.com"
|
108
|
+
Her::API.setup :base_uri => "https://api.example.com" do |builder|
|
109
|
+
builder.delete Her::Middleware::DefaultParseJSON
|
110
|
+
builder.use MyCustomParser
|
111
|
+
end
|
107
112
|
# User.find(1) will now expect "https://api.example.com/users/1" to return something like '{ "data" => { "id": 1, "name": "Tobias Fünke" }, "errors" => [] }'
|
108
113
|
```
|
109
114
|
|
115
|
+
### OAuth
|
116
|
+
|
117
|
+
Using the `faraday_middleware` and `simple_oauth` gems, it’s fairly easy to use OAuth authentication with Her.
|
118
|
+
|
119
|
+
In your Gemfile:
|
120
|
+
|
121
|
+
```ruby
|
122
|
+
gem "her"
|
123
|
+
gem "faraday_middleware"
|
124
|
+
gem "simple_oauth"
|
125
|
+
```
|
126
|
+
|
127
|
+
In your Ruby code:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
# Create an application on `https://dev.twitter.com/apps` to set these values
|
131
|
+
TWITTER_CREDENTIALS = {
|
132
|
+
:consumer_key => "",
|
133
|
+
:consumer_secret => "",
|
134
|
+
:token => "",
|
135
|
+
:token_secret => ""
|
136
|
+
}
|
137
|
+
|
138
|
+
Her::API.setup :base_uri => "https://api.twitter.com/1/" do |builder|
|
139
|
+
builder.use FaradayMiddleware::OAuth, TWITTER_CREDENTIALS
|
140
|
+
end
|
141
|
+
|
142
|
+
class Tweet
|
143
|
+
include Her::Model
|
144
|
+
end
|
145
|
+
|
146
|
+
@tweets = Tweet.get("/statuses/home_timeline.json")
|
147
|
+
```
|
148
|
+
|
149
|
+
### Caching
|
150
|
+
|
151
|
+
Again, using the `faraday_middleware` makes it very easy to cache requests and responses:
|
152
|
+
|
153
|
+
In your Gemfile:
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
gem "her"
|
157
|
+
gem "faraday_middleware"
|
158
|
+
```
|
159
|
+
|
160
|
+
In your Ruby code:
|
161
|
+
|
162
|
+
```ruby
|
163
|
+
class MyCache
|
164
|
+
def write(key, value); end
|
165
|
+
def read(key); end
|
166
|
+
def fetch(key, &block); end
|
167
|
+
end
|
168
|
+
|
169
|
+
# A cache system must respond to `#write`, `#read` and `#fetch`.
|
170
|
+
$cache = MyCache.new
|
171
|
+
|
172
|
+
Her::API.setup :base_uri => "https://api.example.com" do |builder|
|
173
|
+
builder.use FaradayMiddleware::Caching, $cache
|
174
|
+
end
|
175
|
+
|
176
|
+
class User
|
177
|
+
include Her::Model
|
178
|
+
end
|
179
|
+
|
180
|
+
@user = User.find(1)
|
181
|
+
@user = User.find(1) # This request will be fetched from the cache
|
182
|
+
```
|
183
|
+
|
110
184
|
## Relationships
|
111
185
|
|
112
186
|
You can define `has_many`, `has_one` and `belongs_to` relationships in your models. The relationship data is handled in two different ways. When parsing a resource from JSON data, if there’s a relationship data included, it will be used to create new Ruby objects.
|
@@ -255,6 +329,35 @@ User.get("/users/popular") # => [#<User id=1>, #<User id=2>]
|
|
255
329
|
# GET /users/popular
|
256
330
|
```
|
257
331
|
|
332
|
+
## Custom paths
|
333
|
+
|
334
|
+
You can define custom HTTP paths for your models:
|
335
|
+
|
336
|
+
```ruby
|
337
|
+
class User
|
338
|
+
include Her::Model
|
339
|
+
collection_path "/hello_users/:id"
|
340
|
+
end
|
341
|
+
|
342
|
+
@user = User.find(1)
|
343
|
+
# GET /hello_users/1
|
344
|
+
```
|
345
|
+
|
346
|
+
You can include custom variables in your paths:
|
347
|
+
|
348
|
+
```ruby
|
349
|
+
class User
|
350
|
+
include Her::Model
|
351
|
+
collection_path "/organizations/:organization_id/users/:id"
|
352
|
+
end
|
353
|
+
|
354
|
+
@user = User.find(1, :_organization_id => 2)
|
355
|
+
# GET /organizations/2/users/1
|
356
|
+
|
357
|
+
@user = User.all(:_organization_id => 2)
|
358
|
+
# GET /organizations/2/users
|
359
|
+
```
|
360
|
+
|
258
361
|
## Multiple APIs
|
259
362
|
|
260
363
|
It is possible to use different APIs for different models. Instead of calling `Her::API.setup`, you can create instances of `Her::API`:
|
@@ -290,7 +393,6 @@ Category.all
|
|
290
393
|
|
291
394
|
## Things to be done
|
292
395
|
|
293
|
-
* Add support for collection paths of nested resources (eg. `/users/:user_id/comments` for the `Comment` model)
|
294
396
|
* Better error handling
|
295
397
|
* Better documentation
|
296
398
|
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# Create custom parser
|
2
|
+
class TwitterSearchParser < Faraday::Response::Middleware
|
3
|
+
METADATA_KEYS = [:completed_in, :max_id, :max_id_str, :next_page, :page, :query, :refresh_url, :results_per_page, :since_id, :since_id_str]
|
4
|
+
|
5
|
+
def on_complete(env)
|
6
|
+
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
7
|
+
errors = [json.delete(:error)]
|
8
|
+
env[:body] = {
|
9
|
+
:data => json,
|
10
|
+
:errors => errors,
|
11
|
+
:metadata => {},
|
12
|
+
}
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
TWITTER_CREDENTIALS = {
|
17
|
+
:consumer_key => "",
|
18
|
+
:consumer_secret => "",
|
19
|
+
:token => "",
|
20
|
+
:token_secret => ""
|
21
|
+
}
|
22
|
+
|
23
|
+
# Initialize API
|
24
|
+
Her::API.setup :base_uri => "https://api.twitter.com/1/", :parse_middleware => TwitterSearchParser, :add_middleware => [FaradayMiddleware::OAuth => TWITTER_CREDENTIALS]
|
25
|
+
|
26
|
+
# Define classes
|
27
|
+
class Tweet
|
28
|
+
include Her::Model
|
29
|
+
|
30
|
+
def self.timeline
|
31
|
+
get "/statuses/home_timeline.json"
|
32
|
+
end
|
33
|
+
|
34
|
+
def username
|
35
|
+
user[:screen_name]
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
get "/" do
|
40
|
+
@tweets = Tweet.timeline
|
41
|
+
haml :index
|
42
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# Create custom parser
|
2
|
+
class TwitterSearchParser < Faraday::Response::Middleware
|
3
|
+
METADATA_KEYS = [:completed_in, :max_id, :max_id_str, :next_page, :page, :query, :refresh_url, :results_per_page, :since_id, :since_id_str]
|
4
|
+
|
5
|
+
def on_complete(env)
|
6
|
+
json = MultiJson.load(env[:body], :symbolize_keys => true)
|
7
|
+
env[:body] = {
|
8
|
+
:data => json[:results],
|
9
|
+
:errors => [json[:error]],
|
10
|
+
:metadata => json.select { |key, value| METADATA_KEYS.include?(key) }
|
11
|
+
}
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Initialize API
|
16
|
+
Her::API.setup :base_uri => "http://search.twitter.com", :parse_middleware => TwitterSearchParser
|
17
|
+
|
18
|
+
# Define classes
|
19
|
+
class Tweet
|
20
|
+
include Her::Model
|
21
|
+
|
22
|
+
def self.search(query, attrs={})
|
23
|
+
get("/search.json", attrs.merge(:q => query))
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
get "/" do
|
28
|
+
@tweets = Tweet.search("github", :rpp => 30)
|
29
|
+
haml :index
|
30
|
+
end
|
data/her.gemspec
CHANGED
@@ -7,23 +7,27 @@ Gem::Specification.new do |s|
|
|
7
7
|
s.version = Her::VERSION
|
8
8
|
s.authors = ["Rémi Prévost"]
|
9
9
|
s.email = ["remi@exomel.com"]
|
10
|
-
s.homepage = "
|
10
|
+
s.homepage = "http://remiprev.github.com/her"
|
11
11
|
s.summary = "A simple Representational State Transfer-based Hypertext Transfer Protocol-powered Object Relational Mapper. Her?"
|
12
|
-
s.description = "Her is an ORM that maps REST resources to Ruby objects"
|
12
|
+
s.description = "Her is an ORM that maps REST resources and collections to Ruby objects"
|
13
13
|
|
14
14
|
s.files = `git ls-files`.split("\n")
|
15
15
|
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
16
16
|
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
17
17
|
s.require_paths = ["lib"]
|
18
18
|
|
19
|
-
s.add_development_dependency "rake"
|
20
|
-
s.add_development_dependency "rspec"
|
21
|
-
s.add_development_dependency "yard"
|
19
|
+
s.add_development_dependency "rake", "0.9.2.2"
|
20
|
+
s.add_development_dependency "rspec", "2.9.0"
|
21
|
+
s.add_development_dependency "yard", "0.7.5"
|
22
22
|
s.add_development_dependency "redcarpet", "1.17.2"
|
23
|
-
s.add_development_dependency "mocha"
|
24
|
-
s.add_development_dependency "fakeweb"
|
23
|
+
s.add_development_dependency "mocha", "0.11.3"
|
24
|
+
s.add_development_dependency "fakeweb", "1.3.0"
|
25
|
+
s.add_development_dependency "guard", "1.0.1"
|
26
|
+
s.add_development_dependency "guard-rspec", "0.7.0"
|
27
|
+
s.add_development_dependency "rb-fsevent", "0.9.1"
|
28
|
+
s.add_development_dependency "growl", "1.0.3"
|
25
29
|
|
26
|
-
s.add_runtime_dependency "activesupport"
|
27
|
-
s.add_runtime_dependency "faraday"
|
28
|
-
s.add_runtime_dependency "multi_json"
|
30
|
+
s.add_runtime_dependency "activesupport", "3.2.3"
|
31
|
+
s.add_runtime_dependency "faraday", "0.8.0"
|
32
|
+
s.add_runtime_dependency "multi_json", "1.3.4"
|
29
33
|
end
|
data/lib/her.rb
CHANGED
data/lib/her/api.rb
CHANGED
@@ -8,16 +8,20 @@ module Her
|
|
8
8
|
# Setup a default API connection. Accepted arguments and options are the same as {API#setup}.
|
9
9
|
def self.setup(attrs={}) # {{{
|
10
10
|
@@default_api = new
|
11
|
-
@@default_api.setup(attrs)
|
11
|
+
connection = @@default_api.setup(attrs)
|
12
|
+
yield connection.builder if block_given?
|
13
|
+
connection
|
12
14
|
end # }}}
|
13
15
|
|
14
16
|
# Setup the API connection.
|
15
17
|
#
|
16
18
|
# @param [Hash] attrs the options to create a message with
|
17
19
|
# @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::FirstLevelParseJSON} to parse the received JSON
|
20
|
+
# @option attrs [Array, Class] :middleware **Deprecated** A list of the only middleware Her will use
|
21
|
+
# @option attrs [Array, Class] :add_middleware **Deprecated** A list of middleware to add to Her’s default middleware
|
22
|
+
# @option attrs [Class] :parse_middleware **Deprecated** A middleware that will replace {Her::Middleware::FirstLevelParseJSON} to parse the received JSON
|
23
|
+
#
|
24
|
+
# @return Faraday::Connection
|
21
25
|
#
|
22
26
|
# @example Setting up the default API connection
|
23
27
|
# Her::API.setup :base_uri => "https://api.example"
|
@@ -29,7 +33,9 @@ module Her
|
|
29
33
|
# @all.call(env)
|
30
34
|
# end
|
31
35
|
# end
|
32
|
-
# Her::API.setup :base_uri => "https://api.example.com"
|
36
|
+
# Her::API.setup :base_uri => "https://api.example.com" do |builder|
|
37
|
+
# builder.use MyAuthentication
|
38
|
+
# end
|
33
39
|
#
|
34
40
|
# @example A custom parse middleware
|
35
41
|
# class MyCustomParser < Faraday::Response::Middleware
|
@@ -40,7 +46,10 @@ module Her
|
|
40
46
|
# env[:body] = { :data => json, :errors => errors, :metadata => metadata }
|
41
47
|
# end
|
42
48
|
# end
|
43
|
-
# Her::API.setup :base_uri => "https://api.example.com"
|
49
|
+
# Her::API.setup :base_uri => "https://api.example.com" do |builder|
|
50
|
+
# builder.delete Her::Middleware::DefaultParseJSON
|
51
|
+
# builder.use MyCustomParser
|
52
|
+
# end
|
44
53
|
def setup(attrs={}) # {{{
|
45
54
|
@base_uri = attrs[:base_uri]
|
46
55
|
@middleware = Her::API.default_middleware
|
@@ -52,8 +61,18 @@ module Her
|
|
52
61
|
@middleware.flatten!
|
53
62
|
middleware = @middleware
|
54
63
|
@connection = Faraday.new(:url => @base_uri) do |builder|
|
55
|
-
middleware.each
|
64
|
+
middleware.each do |item|
|
65
|
+
klass = item.is_a?(Hash) ? item.keys.first : item
|
66
|
+
args = item.is_a?(Hash) ? item.values.first : nil
|
67
|
+
if args
|
68
|
+
builder.use klass, args
|
69
|
+
else
|
70
|
+
builder.use klass
|
71
|
+
end
|
72
|
+
end
|
56
73
|
end
|
74
|
+
yield @connection.builder if block_given?
|
75
|
+
@connection
|
57
76
|
end # }}}
|
58
77
|
|
59
78
|
# Define a custom parsing procedure. The procedure is passed the response object and is
|
@@ -64,6 +83,7 @@ module Her
|
|
64
83
|
def request(attrs={}) # {{{
|
65
84
|
method = attrs.delete(:_method)
|
66
85
|
path = attrs.delete(:_path)
|
86
|
+
attrs.delete_if { |key, value| key =~ /^_/ } # Remove all internal parameters
|
67
87
|
response = @connection.send method do |request|
|
68
88
|
if method == :get
|
69
89
|
# For GET requests, treat additional parameters as querystring data
|