harpy 0.1.0
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/.gitignore +4 -0
- data/.rspec +3 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README.md +168 -0
- data/Rakefile +8 -0
- data/harpy.gemspec +29 -0
- data/lib/harpy/client.rb +66 -0
- data/lib/harpy/entry_point.rb +35 -0
- data/lib/harpy/resource.rb +197 -0
- data/lib/harpy/version.rb +3 -0
- data/lib/harpy.rb +46 -0
- data/spec/harpy/client_spec.rb +96 -0
- data/spec/harpy/entry_point_spec.rb +56 -0
- data/spec/harpy/resource_spec.rb +516 -0
- data/spec/harpy_spec.rb +97 -0
- data/spec/spec_helper.rb +13 -0
- metadata +159 -0
data/.gitignore
ADDED
data/.rspec
ADDED
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
harpy is Copyright © 2011 TalentBox SA
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,168 @@
|
|
1
|
+
Harpy
|
2
|
+
======
|
3
|
+
|
4
|
+
Client for REST API with HATEOAS
|
5
|
+
|
6
|
+
Dependencies
|
7
|
+
------------
|
8
|
+
|
9
|
+
* Ruby 1.8.7 or 1.9.2
|
10
|
+
* gem "typhoeus", "~> 0.2.4"
|
11
|
+
* gem "activesupport", ">= 3.0.0"
|
12
|
+
* gem "activemodel", ">= 3.0.0"
|
13
|
+
* gem "hash-deep-merge", "~> 0.1.1"
|
14
|
+
* gem "yajl-ruby", "~> 0.8.2"
|
15
|
+
|
16
|
+
Usage
|
17
|
+
-----
|
18
|
+
|
19
|
+
* Set entry_point url:
|
20
|
+
|
21
|
+
Harpy.entry_point_url = "http://localhost"
|
22
|
+
|
23
|
+
* Include `Harpy::Resource` in your model:
|
24
|
+
|
25
|
+
class MyModel
|
26
|
+
include Harpy::Resource
|
27
|
+
end
|
28
|
+
|
29
|
+
# Mass assignment
|
30
|
+
model = MyModel.new "firstname" => "Anthony", "lastname" => "Stark"
|
31
|
+
model.attributes = {"company" => "Stark Enterprises"}
|
32
|
+
model.firstname # => "Anthony"
|
33
|
+
model.company # => "Stark Enterprises"
|
34
|
+
|
35
|
+
# Because model is not persisted you can read any attribute, allowing
|
36
|
+
# to use form_for on new resources to which the client doesn't know the
|
37
|
+
# existing attributes yet
|
38
|
+
model.email # => nil
|
39
|
+
|
40
|
+
# Fetch by url
|
41
|
+
MyModel.from_url "http://localhost/mymodel/1"
|
42
|
+
# => instance of MyModel with attributes filled in on 200
|
43
|
+
# => nil on 404
|
44
|
+
# => raises Harpy::ClientTimeout on timeout
|
45
|
+
# => raises Harpy::ClientError on Curl error
|
46
|
+
# => raises Harpy::InvalidResponseCode on other response codes
|
47
|
+
|
48
|
+
# Fetch multiple by url in parallel
|
49
|
+
MyModel.from_url ["http://localhost/mymodel/1", "http://localhost/mymodel/2"]
|
50
|
+
|
51
|
+
# Get index
|
52
|
+
MyModel.search
|
53
|
+
# will call GET http://localhost/mymodel given the following entry_point response:
|
54
|
+
{
|
55
|
+
"link": [
|
56
|
+
{"rel": "my_model", "href": "http://localhost/mymodel"}
|
57
|
+
]
|
58
|
+
}
|
59
|
+
# => return an array of MyModel instances on 200
|
60
|
+
# => raises Harpy::ClientTimeout on timeout
|
61
|
+
# => raises Harpy::ClientError on Curl error
|
62
|
+
# => raises Harpy::InvalidResponseCode on other response codes
|
63
|
+
|
64
|
+
# Search by first_name
|
65
|
+
MyModel.search :firstname => "Anthony" # GET http://localhost/mymodel?firstname=Anthony
|
66
|
+
|
67
|
+
# Create (POST)
|
68
|
+
model = MyModel.new "firstname" => "Anthony"
|
69
|
+
model.save # POST http://localhost/mymodel with {"firstname":"Anthony"}
|
70
|
+
|
71
|
+
# Get an existing resource by url:
|
72
|
+
model = MyModel.from_url "http://localhost/mymodel/1"
|
73
|
+
# if the service returns the following response:
|
74
|
+
{
|
75
|
+
"firstname": "Anthony",
|
76
|
+
"lastname": null,
|
77
|
+
"urn": "urn:mycompany:mymodel:1"
|
78
|
+
"link" => [
|
79
|
+
{"rel" => "self", "href" => "http://localhost/mymodel/1"},
|
80
|
+
{"rel" => "accounts", "href" => "http://localhost/mymodel/1/accounts"}
|
81
|
+
]
|
82
|
+
}
|
83
|
+
# we can then do:
|
84
|
+
model.firstname # => "Anthony"
|
85
|
+
model.link "self" # => "http://localhost/mymodel/1"
|
86
|
+
model.link :accounts # => "http://localhost/mymodel/1/accounts"
|
87
|
+
|
88
|
+
# Update (PUT) requires resource to have both urn and link to self
|
89
|
+
model.attributes = {"firstname" => "Tony"}
|
90
|
+
model.save # PUT http://localhost/mymodel/1
|
91
|
+
|
92
|
+
# The resource is persisted once it has an urn:
|
93
|
+
model.persisted? # => true
|
94
|
+
|
95
|
+
# If persisted you can no longer read undefined attributes:
|
96
|
+
model.lastname # => nil
|
97
|
+
model.email # => will raise NoMethodError
|
98
|
+
|
99
|
+
* To find a resource by id you need to define `.urn`:
|
100
|
+
|
101
|
+
class MyModel
|
102
|
+
include Harpy::Resource
|
103
|
+
def self.urn(id)
|
104
|
+
"urn:mycompany:mymodel:#{id}"
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
model = MyModel.from_id 1 # will GET http://localhost/urn:mycompany:mymodel:1
|
109
|
+
# expecting a permanent redirect (301) to follow or not found (404)
|
110
|
+
|
111
|
+
* Rel name to search for in entry_point when getting index can be overridden:
|
112
|
+
|
113
|
+
class MyCustomModel
|
114
|
+
include Harpy::Resource
|
115
|
+
def self.resource_name
|
116
|
+
"custom_model"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
* or you can use `.with_url(url)` for getting index of nested resources:
|
121
|
+
|
122
|
+
class Account
|
123
|
+
include Harpy::Resource
|
124
|
+
def users
|
125
|
+
User.with_url(link "user") do
|
126
|
+
User.search
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
class User
|
131
|
+
include Harpy::Resource
|
132
|
+
end
|
133
|
+
|
134
|
+
* you can override `#url_collection` to create nested resources:
|
135
|
+
|
136
|
+
class Account
|
137
|
+
include Harpy::Resource
|
138
|
+
end
|
139
|
+
class User
|
140
|
+
include Harpy::Resource
|
141
|
+
attr_accessor :account
|
142
|
+
def url_collection
|
143
|
+
account ? account.link("user") : super
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
* Fetch multiple resources in parallel:
|
148
|
+
|
149
|
+
class FirstModel
|
150
|
+
include Harpy::Resource
|
151
|
+
end
|
152
|
+
class SecondModel
|
153
|
+
include Harpy::Resource
|
154
|
+
end
|
155
|
+
|
156
|
+
Harpy::Resource.from_url({
|
157
|
+
FirstModel => ["http://localhost/firstmodel/1", "http://localhost/firstmodel/2"],
|
158
|
+
SecondModel => ["http://localhost/secondmodel/1"],
|
159
|
+
})
|
160
|
+
# => {FirstModel => [...], SecondModel => [...]}
|
161
|
+
|
162
|
+
About
|
163
|
+
-----
|
164
|
+
|
165
|
+
License
|
166
|
+
-------
|
167
|
+
|
168
|
+
harpy is Copyright © 2011 TalentBox SA. It is free software, and may be redistributed under the terms specified in the LICENSE file.
|
data/Rakefile
ADDED
data/harpy.gemspec
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "harpy/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "harpy"
|
7
|
+
s.version = Harpy::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Joseph HALTER", "Jonathan TRON"]
|
10
|
+
s.email = ["joseph.halter@thetalentbox.com", "jonathan.tron@thetalentbox.com"]
|
11
|
+
s.homepage = "https://github.com/TalentBox/harpy"
|
12
|
+
s.summary = %q{Client for REST API}
|
13
|
+
s.description = %q{Client for REST API with HATEOAS}
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
s.add_runtime_dependency("typhoeus", ["~> 0.2.4"])
|
21
|
+
s.add_runtime_dependency("activesupport", [">= 3.0.0"])
|
22
|
+
s.add_runtime_dependency("activemodel", [">= 3.0.0"])
|
23
|
+
s.add_runtime_dependency("hash-deep-merge", ["~> 0.1.1"])
|
24
|
+
s.add_runtime_dependency("yajl-ruby", ["~> 0.8.2"])
|
25
|
+
|
26
|
+
s.add_development_dependency("rake", ["~> 0.8.7"])
|
27
|
+
s.add_development_dependency("rspec", ["~> 2.6.0"])
|
28
|
+
s.add_development_dependency("rocco", ["~> 0.7"])
|
29
|
+
end
|
data/lib/harpy/client.rb
ADDED
@@ -0,0 +1,66 @@
|
|
1
|
+
require "typhoeus"
|
2
|
+
require "hash_deep_merge"
|
3
|
+
|
4
|
+
module Harpy
|
5
|
+
class Client
|
6
|
+
attr_accessor :options
|
7
|
+
|
8
|
+
def initialize(opts=nil)
|
9
|
+
self.options = (opts || {})
|
10
|
+
end
|
11
|
+
|
12
|
+
def get(url_or_urls, opts=nil)
|
13
|
+
request :get, url_or_urls, opts
|
14
|
+
end
|
15
|
+
|
16
|
+
def head(url_or_urls, opts=nil)
|
17
|
+
request :head, url_or_urls, opts
|
18
|
+
end
|
19
|
+
|
20
|
+
def post(url_or_urls, opts=nil)
|
21
|
+
request :post, url_or_urls, opts
|
22
|
+
end
|
23
|
+
|
24
|
+
def put(url_or_urls, opts=nil)
|
25
|
+
request :put, url_or_urls, opts
|
26
|
+
end
|
27
|
+
|
28
|
+
def patch(url_or_urls, opts=nil)
|
29
|
+
request :patch, url_or_urls, opts
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(url_or_urls, opts=nil)
|
33
|
+
request :delete, url_or_urls, opts
|
34
|
+
end
|
35
|
+
|
36
|
+
def run(requests)
|
37
|
+
requests.each{|request| Typhoeus::Hydra.hydra.queue request}
|
38
|
+
Typhoeus::Hydra.hydra.run
|
39
|
+
requests.collect(&:response)
|
40
|
+
end
|
41
|
+
|
42
|
+
def invalid_code(response)
|
43
|
+
if response.timed_out?
|
44
|
+
raise ClientTimeout
|
45
|
+
elsif response.code.zero?
|
46
|
+
raise ClientError, response.curl_error_message
|
47
|
+
else
|
48
|
+
raise InvalidResponseCode, response.code.to_s
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def request(method, urls, opts=nil)
|
55
|
+
opts = options.deep_merge(opts || {})
|
56
|
+
case urls
|
57
|
+
when Array
|
58
|
+
requests = urls.collect do |url|
|
59
|
+
Typhoeus::Request.new url, opts.merge(:method => method)
|
60
|
+
end
|
61
|
+
else
|
62
|
+
Typhoeus::Request.run urls, opts.merge(:method => method)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require "yajl"
|
2
|
+
|
3
|
+
module Harpy
|
4
|
+
class EntryPoint
|
5
|
+
attr_accessor :url
|
6
|
+
|
7
|
+
def initialize(url)
|
8
|
+
self.url = url
|
9
|
+
end
|
10
|
+
|
11
|
+
def resource_url(resource_type)
|
12
|
+
response = Harpy.client.get url
|
13
|
+
case response.code
|
14
|
+
when 200
|
15
|
+
body = Yajl::Parser.parse response.body
|
16
|
+
link = (body["link"] || []).detect{|link| link["rel"] == resource_type}
|
17
|
+
link["href"] if link
|
18
|
+
else
|
19
|
+
Harpy.client.invalid_code response
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def urn(urn)
|
24
|
+
response = Harpy.client.get "#{url}/#{urn}"
|
25
|
+
case response.code
|
26
|
+
when 301
|
27
|
+
response.headers_hash["Location"]
|
28
|
+
when 404
|
29
|
+
nil
|
30
|
+
else
|
31
|
+
Harpy.client.invalid_code response
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,197 @@
|
|
1
|
+
require "harpy/client"
|
2
|
+
require "active_support"
|
3
|
+
require "active_support/core_ext/object/blank"
|
4
|
+
require "active_support/core_ext/numeric/bytes"
|
5
|
+
require "active_model"
|
6
|
+
require "yajl"
|
7
|
+
|
8
|
+
module Harpy
|
9
|
+
module Resource
|
10
|
+
extend ActiveSupport::Concern
|
11
|
+
|
12
|
+
included do |base|
|
13
|
+
base.extend ActiveModel::Naming
|
14
|
+
base.send :include, ActiveModel::Conversion
|
15
|
+
base.extend ActiveModel::Translation
|
16
|
+
base.extend ActiveModel::Callbacks
|
17
|
+
base.send :include, ActiveModel::Validations
|
18
|
+
base.send :include, ActiveModel::Validations::Callbacks
|
19
|
+
base.define_model_callbacks :save, :create, :update, :destroy, :only => [:before, :after]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.from_url(hash)
|
23
|
+
results = {}
|
24
|
+
hash.each do |klass, urls|
|
25
|
+
results[klass] = Harpy.client.get [*urls]
|
26
|
+
end
|
27
|
+
Harpy.client.run results.values.flatten
|
28
|
+
results.each do |klass, requests|
|
29
|
+
requests.collect! do |request|
|
30
|
+
klass.send :from_url_handler, request.response
|
31
|
+
end
|
32
|
+
end
|
33
|
+
results
|
34
|
+
end
|
35
|
+
|
36
|
+
module ClassMethods
|
37
|
+
def from_url(url)
|
38
|
+
case url
|
39
|
+
when Array
|
40
|
+
Harpy.client.run(client.get url).collect{|response| from_url_handler response}
|
41
|
+
else
|
42
|
+
from_url_handler Harpy.client.get url
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def from_id(id)
|
47
|
+
url = Harpy.entry_point.urn urn(id)
|
48
|
+
from_url url if url
|
49
|
+
end
|
50
|
+
|
51
|
+
def urn(id)
|
52
|
+
raise NotImplementedError
|
53
|
+
end
|
54
|
+
|
55
|
+
def resource_name
|
56
|
+
name.underscore
|
57
|
+
end
|
58
|
+
|
59
|
+
def search(conditions={})
|
60
|
+
response = Harpy.client.get url, :params => conditions
|
61
|
+
case response.code
|
62
|
+
when 200
|
63
|
+
parsed = Yajl::Parser.parse response.body
|
64
|
+
parsed[resource_name].collect{|model| new model}
|
65
|
+
else
|
66
|
+
Harpy.client.invalid_code response
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def with_url(url)
|
71
|
+
raise ArgumentError unless block_given?
|
72
|
+
key = "#{resource_name}_url"
|
73
|
+
old, Thread.current[key] = Thread.current[key], url
|
74
|
+
result = yield
|
75
|
+
Thread.current[key] = old
|
76
|
+
result
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def url
|
82
|
+
Thread.current["#{resource_name}_url"] || Harpy.entry_point.resource_url(resource_name)
|
83
|
+
end
|
84
|
+
|
85
|
+
def from_url_handler(response)
|
86
|
+
case response.code
|
87
|
+
when 200
|
88
|
+
new Yajl::Parser.parse response.body
|
89
|
+
when 404
|
90
|
+
nil
|
91
|
+
else
|
92
|
+
Harpy.client.invalid_code response
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
module InstanceMethods
|
98
|
+
def initialize(attrs = {})
|
99
|
+
@attrs = attrs
|
100
|
+
end
|
101
|
+
|
102
|
+
def attributes=(attrs)
|
103
|
+
@attrs.merge! attrs
|
104
|
+
end
|
105
|
+
|
106
|
+
def as_json
|
107
|
+
hash = @attrs.dup
|
108
|
+
hash.delete "link"
|
109
|
+
hash.delete "urn"
|
110
|
+
hash
|
111
|
+
end
|
112
|
+
|
113
|
+
def save
|
114
|
+
if valid?
|
115
|
+
_run_save_callbacks do
|
116
|
+
json = Yajl::Encoder.encode as_json
|
117
|
+
raise BodyToBig, "Size: #{json.bytesize} bytes (max 1MB)" if json.bytesize > 1.megabyte
|
118
|
+
persisted? ? update(json) : create(json)
|
119
|
+
end
|
120
|
+
else
|
121
|
+
false
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def link(rel)
|
126
|
+
link = (@attrs["link"]||[]).detect{|l| l["rel"]==rel.to_s}
|
127
|
+
link["href"] if link
|
128
|
+
end
|
129
|
+
|
130
|
+
def url
|
131
|
+
link "self"
|
132
|
+
end
|
133
|
+
|
134
|
+
def url_collection
|
135
|
+
Harpy.entry_point.resource_url self.class.resource_name
|
136
|
+
end
|
137
|
+
|
138
|
+
def id
|
139
|
+
@attrs["urn"].split(":").last if @attrs["urn"]
|
140
|
+
end
|
141
|
+
|
142
|
+
def persisted?
|
143
|
+
@attrs["urn"].present?
|
144
|
+
end
|
145
|
+
|
146
|
+
def inspect
|
147
|
+
"<#{self.class.name} @attrs:#{@attrs.inspect} @errors:#{errors.inspect} persisted:#{persisted?}>"
|
148
|
+
end
|
149
|
+
|
150
|
+
def has_key?(key)
|
151
|
+
@attrs.has_key? key.to_s
|
152
|
+
end
|
153
|
+
|
154
|
+
private
|
155
|
+
|
156
|
+
def create(json)
|
157
|
+
_run_create_callbacks do
|
158
|
+
process_response Harpy.client.post(url_collection, :body => json), :create
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def update(json)
|
163
|
+
_run_update_callbacks do
|
164
|
+
raise Harpy::UrlRequired unless url
|
165
|
+
process_response Harpy.client.put(url, :body => json), :update
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
def process_response(response, context)
|
170
|
+
case response.code
|
171
|
+
when 200, 201, 302
|
172
|
+
@attrs.merge! Yajl::Parser.parse(response.body)
|
173
|
+
true
|
174
|
+
when 204
|
175
|
+
context==:create ? Harpy.client.invalid_code(response) : true
|
176
|
+
when 401
|
177
|
+
raise Harpy::Unauthorized, "Server returned a 401 response code"
|
178
|
+
when 422
|
179
|
+
Yajl::Parser.parse(response.body)["errors"].each do |attr, attr_errors|
|
180
|
+
attr_errors.each{|attr_error| errors[attr] = attr_error }
|
181
|
+
end
|
182
|
+
false
|
183
|
+
else
|
184
|
+
Harpy.client.invalid_code response
|
185
|
+
end
|
186
|
+
end
|
187
|
+
|
188
|
+
def method_missing(method, *args)
|
189
|
+
if persisted? && !@attrs.has_key?(method.to_s)
|
190
|
+
super
|
191
|
+
else
|
192
|
+
@attrs[method.to_s]
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
196
|
+
end
|
197
|
+
end
|
data/lib/harpy.rb
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
module Harpy
|
2
|
+
class Exception < ::Exception; end
|
3
|
+
class EntryPointRequired < Exception; end
|
4
|
+
class UrlRequired < Exception; end
|
5
|
+
class BodyToBig < Exception; end
|
6
|
+
class ClientTimeout < Exception; end
|
7
|
+
class ClientError < Exception; end
|
8
|
+
class Unauthorized < Exception; end
|
9
|
+
class InvalidResponseCode < Exception; end
|
10
|
+
|
11
|
+
autoload :Client, "harpy/client"
|
12
|
+
autoload :EntryPoint, "harpy/entry_point"
|
13
|
+
autoload :Resource, "harpy/resource"
|
14
|
+
autoload :BodyToBig, "harpy/resource"
|
15
|
+
autoload :UnknownResponseCode, "harpy/resource"
|
16
|
+
|
17
|
+
def self.client=(new_client)
|
18
|
+
@client = new_client
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.client
|
22
|
+
@client ||= Client.new
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.entry_point_url=(url)
|
26
|
+
@entry_point = EntryPoint.new url
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.entry_point_url
|
30
|
+
@entry_point.url if @entry_point
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.entry_point=(value)
|
34
|
+
@entry_point = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def self.entry_point
|
38
|
+
@entry_point || raise(EntryPointRequired, 'you can setup one with Harpy.entry_point_url = "http://localhost"')
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.reset
|
42
|
+
@client = nil
|
43
|
+
@entry_point = nil
|
44
|
+
end
|
45
|
+
|
46
|
+
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Harpy::Client do
|
4
|
+
let(:entry_url) { "http://localhost" }
|
5
|
+
let(:users_url) { "http://localhost/users" }
|
6
|
+
|
7
|
+
context "by default" do
|
8
|
+
its(:options) { should be_empty }
|
9
|
+
end
|
10
|
+
|
11
|
+
context "initialized with options" do
|
12
|
+
let(:options) { {:username => "harpy", :password => "spec"} }
|
13
|
+
subject { Harpy::Client.new(options) }
|
14
|
+
its(:options) { should == options }
|
15
|
+
end
|
16
|
+
|
17
|
+
[:get, :head, :post, :put, :patch, :delete].each do |method|
|
18
|
+
describe "##{method}(url, opts={})" do
|
19
|
+
context "with one url" do
|
20
|
+
before do
|
21
|
+
@expected = Typhoeus::Response.new :code => 200
|
22
|
+
Typhoeus::Hydra.hydra.stub(method, entry_url).and_return(@expected)
|
23
|
+
end
|
24
|
+
it "sends a #{method.to_s.upcase} to the url" do
|
25
|
+
subject.send(method, entry_url).should == @expected
|
26
|
+
end
|
27
|
+
it "merges options" do
|
28
|
+
client = Harpy::Client.new :headers => {"Authorization" => "spec"}
|
29
|
+
Typhoeus::Hydra.hydra.stub(method, entry_url).and_return(Typhoeus::Response.new :code => 200)
|
30
|
+
response = client.send method, entry_url, :headers => {"X-Files" => "Harpy"}
|
31
|
+
response.request.headers.should include({"X-Files" => "Harpy", "Authorization" => "spec"})
|
32
|
+
end
|
33
|
+
end
|
34
|
+
context "with multiple urls" do
|
35
|
+
it "does not execute requests" do
|
36
|
+
lambda {
|
37
|
+
subject.send method, [entry_url, users_url]
|
38
|
+
}.should_not raise_error Typhoeus::Hydra::NetConnectNotAllowedError
|
39
|
+
end
|
40
|
+
it "returns one requests per url" do
|
41
|
+
requests = subject.send method, [entry_url, users_url]
|
42
|
+
requests.size.should == 2
|
43
|
+
requests.collect(&:method).should =~ [method, method]
|
44
|
+
requests.collect(&:url).should =~ [entry_url, users_url]
|
45
|
+
end
|
46
|
+
it "merges options" do
|
47
|
+
client = Harpy::Client.new :headers => {"Authorization" => "spec"}
|
48
|
+
requests = client.send method, [entry_url, users_url], :headers => {"X-Files" => "Harpy"}
|
49
|
+
requests.each do |request|
|
50
|
+
request.headers.should include({"X-Files" => "Harpy", "Authorization" => "spec"})
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
describe "#run(requests)" do
|
57
|
+
before do
|
58
|
+
@entry_response = Typhoeus::Response.new :code => 200, :body => "entry"
|
59
|
+
@users_response = Typhoeus::Response.new :code => 200, :body => "users"
|
60
|
+
|
61
|
+
Typhoeus::Hydra.hydra.stub(:get, entry_url).and_return @entry_response
|
62
|
+
Typhoeus::Hydra.hydra.stub(:get, users_url).and_return @users_response
|
63
|
+
end
|
64
|
+
it "executes requests in parallel" do
|
65
|
+
Typhoeus::Hydra.hydra.should_receive(:run).once
|
66
|
+
subject.run subject.get([entry_url, users_url])
|
67
|
+
end
|
68
|
+
it "returns responses" do
|
69
|
+
responses = subject.run subject.get([entry_url, users_url])
|
70
|
+
responses.should =~ [@entry_response, @users_response]
|
71
|
+
end
|
72
|
+
it "requests response is filled in" do
|
73
|
+
requests = subject.get([entry_url, users_url])
|
74
|
+
subject.run requests
|
75
|
+
requests[0].response.should == @entry_response
|
76
|
+
requests[1].response.should == @users_response
|
77
|
+
end
|
78
|
+
end
|
79
|
+
describe "#invalid_code(response)" do
|
80
|
+
it "raises Harpy::ClientTimeout on request timeout" do
|
81
|
+
lambda {
|
82
|
+
subject.invalid_code mock("Response", :timed_out? => true)
|
83
|
+
}.should raise_error Harpy::ClientTimeout
|
84
|
+
end
|
85
|
+
it "raises Harpy::ClientError on code 0" do
|
86
|
+
lambda {
|
87
|
+
subject.invalid_code mock("Response", :timed_out? => false, :code => 0, :curl_error_message => "Could not connect to server")
|
88
|
+
}.should raise_error Harpy::ClientError, "Could not connect to server"
|
89
|
+
end
|
90
|
+
it "raises Harpy::InvalidResponseCode with code otherwise" do
|
91
|
+
lambda {
|
92
|
+
subject.invalid_code mock("Response", :timed_out? => false, :code => 404)
|
93
|
+
}.should raise_error Harpy::InvalidResponseCode, "404"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|