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 ADDED
@@ -0,0 +1,7 @@
1
+ require "guard/guard"
2
+
3
+ guard "rspec", :cli => "--colour --format=documentation" do
4
+ watch(%r{^spec/.+_spec\.rb$})
5
+ watch(%r{^lib/.+\.rb$}) { "spec" }
6
+ watch("spec/spec_helper.rb") { "spec" }
7
+ end
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 `add_middleware` key, we add it to the default list of middleware.
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
- @all.call(env)
69
+ @app.call(env)
70
70
  end
71
71
  end
72
72
 
73
- Her::API.setup :base_uri => "https://api.example.com", :add_middleware => [MyAuthentication]
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 `parse_middleware` key, we then replace the default parser.
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", :parse_middleware => MyCustomParser
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,13 @@
1
+ source :rubygems
2
+
3
+ gem "sinatra"
4
+ gem "haml"
5
+ gem "thin", :require => false
6
+ gem "faraday_middleware"
7
+ gem "simple_oauth"
8
+
9
+ gem "her", :path => File.join(File.dirname(__FILE__),"../..")
10
+
11
+ group :development do
12
+ gem "shotgun"
13
+ end
@@ -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,5 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "./app"
5
+ run Sinatra::Application
@@ -0,0 +1,9 @@
1
+ !!!
2
+ %html
3
+ %body
4
+ %ul
5
+ - @tweets.each do |tweet|
6
+ %li{ :style => "margin: 0 0 10px" }
7
+ %span= tweet.text
8
+ %br
9
+ %strong= tweet.username
@@ -0,0 +1,11 @@
1
+ source :rubygems
2
+
3
+ gem "sinatra"
4
+ gem "haml"
5
+ gem "thin", :require => false
6
+
7
+ gem "her", :path => File.join(File.dirname(__FILE__),"../..")
8
+
9
+ group :development do
10
+ gem "shotgun"
11
+ 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
@@ -0,0 +1,5 @@
1
+ require "bundler"
2
+ Bundler.require
3
+
4
+ require "./app"
5
+ run Sinatra::Application
@@ -0,0 +1,9 @@
1
+ !!!
2
+ %html
3
+ %body
4
+ %ul
5
+ - @tweets.each do |tweet|
6
+ %li{ :style => "margin: 0 0 10px" }
7
+ %span= tweet.text
8
+ %br
9
+ %strong= tweet.from_user
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 = "https://github.com/remiprev/her"
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
@@ -8,4 +8,5 @@ module Her
8
8
  autoload :Model, "her/model"
9
9
  autoload :API, "her/api"
10
10
  autoload :Middleware, "her/middleware"
11
+ autoload :Errors, "her/errors"
11
12
  end
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", :add_middleware => [MyAuthentication]
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", :parse_middleware => MyCustomParser
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 { |m| builder.use(m) }
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
data/lib/her/errors.rb ADDED
@@ -0,0 +1,5 @@
1
+ module Her
2
+ module Errors
3
+ class PathError < StandardError; end;
4
+ end
5
+ end