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 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