her 0.2 → 0.2.1
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/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
|
-
[](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
|
+
[](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
|