voorhees 0.2.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 +5 -0
- data/LICENSE +20 -0
- data/README.markdown +240 -0
- data/Rakefile +55 -0
- data/VERSION +1 -0
- data/examples/twitter.rb +85 -0
- data/lib/voorhees.rb +6 -0
- data/lib/voorhees/config.rb +87 -0
- data/lib/voorhees/exceptions.rb +9 -0
- data/lib/voorhees/logging.rb +5 -0
- data/lib/voorhees/request.rb +146 -0
- data/lib/voorhees/resource.rb +141 -0
- data/lib/voorhees/response.rb +36 -0
- data/spec/config_spec.rb +144 -0
- data/spec/fixtures/resources.rb +18 -0
- data/spec/fixtures/user.json +32 -0
- data/spec/fixtures/users.json +1 -0
- data/spec/request_spec.rb +455 -0
- data/spec/resource_spec.rb +335 -0
- data/spec/response_spec.rb +93 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/voorhees_spec.rb +1 -0
- data/voorhees.gemspec +65 -0
- metadata +85 -0
@@ -0,0 +1,9 @@
|
|
1
|
+
module Voorhees
|
2
|
+
class Error < ::StandardError; end
|
3
|
+
class ParameterMissingError < Error; end
|
4
|
+
class NotFoundError < Error; end
|
5
|
+
class TimeoutError < Error; end
|
6
|
+
class UnavailableError < Error; end
|
7
|
+
class ParseError < Error; end
|
8
|
+
class NotResourceError < Error; end
|
9
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'net/http'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'system_timer'
|
6
|
+
VoorheesTimer = SystemTimer
|
7
|
+
rescue LoadError
|
8
|
+
require 'timeout'
|
9
|
+
VoorheesTimer = Timeout
|
10
|
+
end
|
11
|
+
|
12
|
+
module Voorhees
|
13
|
+
|
14
|
+
class Request
|
15
|
+
|
16
|
+
attr_accessor :timeout, :retries, :path,
|
17
|
+
:required, :defaults, :parameters,
|
18
|
+
:base_uri, :http_method, :hierarchy
|
19
|
+
|
20
|
+
def initialize(caller_class=nil)
|
21
|
+
@caller_class = caller_class
|
22
|
+
end
|
23
|
+
|
24
|
+
def path=(uri)
|
25
|
+
@path = URI.parse(uri)
|
26
|
+
end
|
27
|
+
|
28
|
+
def uri
|
29
|
+
u = path.relative? ? URI.parse("#{base_uri}#{path}") : path
|
30
|
+
if query = query_string(u)
|
31
|
+
u.query = query
|
32
|
+
end
|
33
|
+
u
|
34
|
+
end
|
35
|
+
|
36
|
+
def base_uri
|
37
|
+
@base_uri || Voorhees::Config[:base_uri]
|
38
|
+
end
|
39
|
+
|
40
|
+
def defaults
|
41
|
+
@defaults || Voorhees::Config[:defaults]
|
42
|
+
end
|
43
|
+
|
44
|
+
def parameters
|
45
|
+
(defaults || {}).merge(@parameters || {})
|
46
|
+
end
|
47
|
+
|
48
|
+
def timeout
|
49
|
+
@timeout || Voorhees::Config[:timeout]
|
50
|
+
end
|
51
|
+
|
52
|
+
def retries
|
53
|
+
@retries || Voorhees::Config[:retries]
|
54
|
+
end
|
55
|
+
|
56
|
+
def http_method
|
57
|
+
@http_method || Voorhees::Config[:http_method]
|
58
|
+
end
|
59
|
+
|
60
|
+
def perform
|
61
|
+
setup_request
|
62
|
+
build_response(perform_actual_request)
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
|
67
|
+
def setup_request
|
68
|
+
@http = Net::HTTP.new(uri.host, uri.port)
|
69
|
+
@req = http_method.new(uri.path)
|
70
|
+
|
71
|
+
@req.form_data = if Voorhees::Config[:post_json]
|
72
|
+
{ Voorhees::Config[:post_json_parameter] => parameters.to_json }
|
73
|
+
else
|
74
|
+
parameters
|
75
|
+
end
|
76
|
+
|
77
|
+
@http.open_timeout = timeout
|
78
|
+
@http.read_timeout = timeout
|
79
|
+
end
|
80
|
+
|
81
|
+
def perform_actual_request
|
82
|
+
retries_left = retries
|
83
|
+
|
84
|
+
Voorhees.debug("Performing #{http_method} request for #{uri.to_s}")
|
85
|
+
|
86
|
+
begin
|
87
|
+
retries_left -= 1
|
88
|
+
|
89
|
+
response = VoorheesTimer.timeout(timeout) do
|
90
|
+
@http.start do |connection|
|
91
|
+
connection.request(@req)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
rescue Timeout::Error
|
96
|
+
if retries_left >= 0
|
97
|
+
Voorhees.debug("Retrying due to Timeout::Error (#{uri.to_s})")
|
98
|
+
retry
|
99
|
+
end
|
100
|
+
|
101
|
+
Voorhees.debug("Request failed due to Timeout::Error (#{uri.to_s})")
|
102
|
+
raise Voorhees::TimeoutError.new
|
103
|
+
|
104
|
+
rescue Errno::ECONNREFUSED
|
105
|
+
if retries_left >= 0
|
106
|
+
Voorhees.debug("Retrying due to Errno::ECONNREFUSED (#{uri.to_s})")
|
107
|
+
sleep(1)
|
108
|
+
retry
|
109
|
+
end
|
110
|
+
|
111
|
+
Voorhees.debug("Request failed due to Errno::ECONREFUSED (#{uri.to_s})")
|
112
|
+
raise Voorhees::UnavailableError.new
|
113
|
+
|
114
|
+
end
|
115
|
+
|
116
|
+
if response.is_a?(Net::HTTPNotFound)
|
117
|
+
Voorhees.debug("Request failed due to Net::HTTPNotFound (#{uri.to_s})")
|
118
|
+
raise Voorhees::NotFoundError.new
|
119
|
+
end
|
120
|
+
|
121
|
+
response
|
122
|
+
end
|
123
|
+
|
124
|
+
def query_string(uri)
|
125
|
+
return if post?
|
126
|
+
query_string_parts = []
|
127
|
+
query_string_parts << uri.query unless uri.query.blank?
|
128
|
+
query_string_parts += parameters.collect{|k,v| "#{k}=#{v}" } unless parameters.empty?
|
129
|
+
query_string_parts.size > 0 ? query_string_parts.join('&') : nil
|
130
|
+
end
|
131
|
+
|
132
|
+
def build_response(response)
|
133
|
+
Voorhees::Config[:response_class].new(response.body, @caller_class, @hierarchy)
|
134
|
+
end
|
135
|
+
|
136
|
+
def validate
|
137
|
+
raise Voorhees::ParameterMissingError if @required && !@required.all?{|x| @parameters.keys.include?(x) }
|
138
|
+
end
|
139
|
+
|
140
|
+
def post?
|
141
|
+
http_method == Net::HTTP::Post
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
@@ -0,0 +1,141 @@
|
|
1
|
+
module Voorhees
|
2
|
+
|
3
|
+
module Resource
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
base.send :include, InstanceMethods
|
8
|
+
|
9
|
+
base.instance_eval do
|
10
|
+
attr_accessor :raw_json, :json_hierarchy
|
11
|
+
undef_method :id
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module ClassMethods
|
16
|
+
def new_from_json(json, hierarchy=nil)
|
17
|
+
obj = self.new
|
18
|
+
obj.raw_json = json
|
19
|
+
obj.json_hierarchy = hierarchy
|
20
|
+
obj
|
21
|
+
end
|
22
|
+
|
23
|
+
def json_service(name, request_options={})
|
24
|
+
klass = request_options.delete(:class) || self
|
25
|
+
(class << self; self; end).instance_eval do
|
26
|
+
define_method name do |*args|
|
27
|
+
params = args[0]
|
28
|
+
json_request(:class => klass) do |r|
|
29
|
+
r.parameters = params if params.is_a?(Hash)
|
30
|
+
request_options.each do |option, value|
|
31
|
+
r.send("#{option}=", value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def json_request(options={})
|
39
|
+
request = Voorhees::Request.new(options[:class] || self)
|
40
|
+
yield request
|
41
|
+
response = request.perform
|
42
|
+
|
43
|
+
case options[:returning]
|
44
|
+
when :raw
|
45
|
+
response.body
|
46
|
+
when :json
|
47
|
+
response.json
|
48
|
+
when :response
|
49
|
+
response
|
50
|
+
else
|
51
|
+
response.to_objects
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
module InstanceMethods
|
57
|
+
|
58
|
+
def json_attributes
|
59
|
+
@json_attributes ||= @raw_json.keys.collect{|x| x.underscore.to_sym}
|
60
|
+
end
|
61
|
+
|
62
|
+
def json_request(options={})
|
63
|
+
self.class.json_request(options) do |r|
|
64
|
+
yield r
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def method_missing(*args)
|
69
|
+
method_name = args[0]
|
70
|
+
if json_attributes.include?(method_name)
|
71
|
+
value = value_from_json(method_name)
|
72
|
+
build_methods(method_name, value)
|
73
|
+
value
|
74
|
+
elsif method_name.to_s =~ /(.+)=$/ && json_attributes.include?($1.to_sym)
|
75
|
+
build_methods($1, args[1])
|
76
|
+
else
|
77
|
+
super
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def value_from_json(method_name)
|
84
|
+
item = raw_json[method_name.to_s] || raw_json[method_name.to_s.camelize(:lower)]
|
85
|
+
|
86
|
+
sub_hierarchy = nil
|
87
|
+
if json_hierarchy && hierarchy = json_hierarchy[method_name]
|
88
|
+
if hierarchy.is_a?(Array)
|
89
|
+
klass = hierarchy[0]
|
90
|
+
sub_hierarchy = hierarchy[1]
|
91
|
+
else
|
92
|
+
klass = hierarchy
|
93
|
+
end
|
94
|
+
|
95
|
+
klass = Object.const_get(klass.to_s.pluralize.classify) if klass.is_a?(Symbol)
|
96
|
+
end
|
97
|
+
|
98
|
+
if item.is_a?(Array)
|
99
|
+
build_collection_from_json(method_name, item, klass, sub_hierarchy)
|
100
|
+
else
|
101
|
+
build_item(item, klass, sub_hierarchy)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_methods(name, value)
|
106
|
+
self.instance_variable_set("@#{name}".to_sym, value)
|
107
|
+
|
108
|
+
instance_eval "
|
109
|
+
def #{name}
|
110
|
+
@#{name} ||= value_from_json(:#{name})
|
111
|
+
end
|
112
|
+
|
113
|
+
def #{name}=(val)
|
114
|
+
@#{name} = val
|
115
|
+
end
|
116
|
+
"
|
117
|
+
end
|
118
|
+
|
119
|
+
def build_item(json, klass, hierarchy)
|
120
|
+
if klass
|
121
|
+
raise Voorhees::NotResourceError.new unless klass.respond_to?(:new_from_json)
|
122
|
+
klass.new_from_json(json, hierarchy)
|
123
|
+
else
|
124
|
+
json
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
def build_collection_from_json(name, json, klass, hierarchy)
|
129
|
+
klass ||= Object.const_get(name.to_s.classify)
|
130
|
+
json.collect do |item|
|
131
|
+
klass.new_from_json(json, hierarchy)
|
132
|
+
end
|
133
|
+
rescue NameError
|
134
|
+
json
|
135
|
+
end
|
136
|
+
|
137
|
+
end
|
138
|
+
|
139
|
+
end
|
140
|
+
|
141
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Voorhees
|
2
|
+
|
3
|
+
class Response
|
4
|
+
|
5
|
+
attr_reader :body, :klass
|
6
|
+
|
7
|
+
def initialize(body, klass=nil, hierarchy=nil)
|
8
|
+
@body = body
|
9
|
+
@hierarchy = hierarchy
|
10
|
+
@klass = klass
|
11
|
+
end
|
12
|
+
|
13
|
+
def json
|
14
|
+
@json ||= JSON.parse(@body)
|
15
|
+
rescue JSON::ParserError
|
16
|
+
Voorhees.debug("Parsing JSON failed.\nFirst 500 chars of body:\n#{response.body[0...500]}")
|
17
|
+
raise Voorhees::ParseError
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_objects
|
21
|
+
return unless @klass
|
22
|
+
|
23
|
+
raise Voorhees::NotResourceError.new unless @klass.respond_to?(:new_from_json)
|
24
|
+
|
25
|
+
if json.is_a?(Array)
|
26
|
+
json.collect do |item|
|
27
|
+
@klass.new_from_json(item, @hierarchy)
|
28
|
+
end
|
29
|
+
else
|
30
|
+
@klass.new_from_json(json, @hierarchy)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
35
|
+
|
36
|
+
end
|
data/spec/config_spec.rb
ADDED
@@ -0,0 +1,144 @@
|
|
1
|
+
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
2
|
+
|
3
|
+
describe Voorhees::Config do
|
4
|
+
|
5
|
+
before :each do
|
6
|
+
|
7
|
+
Voorhees::Config.clear
|
8
|
+
Voorhees::Config.setup do |c|
|
9
|
+
c[:one] = 1
|
10
|
+
c[:two] = 2
|
11
|
+
end
|
12
|
+
|
13
|
+
end
|
14
|
+
|
15
|
+
describe "logger" do
|
16
|
+
|
17
|
+
before :each do
|
18
|
+
Object.send(:remove_const, :RAILS_DEFAULT_LOGGER) if defined?(RAILS_DEFAULT_LOGGER)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "should default to RAILS_DEFAULT_LOGGER if defined" do
|
22
|
+
RAILS_DEFAULT_LOGGER = "something"
|
23
|
+
Voorhees::Config.reset
|
24
|
+
Voorhees::Config.logger.should == "something"
|
25
|
+
end
|
26
|
+
|
27
|
+
it "should default to a Logger if RAILS_DEFAULT_LOGGER is not defined" do
|
28
|
+
Voorhees::Config.reset
|
29
|
+
Voorhees::Config.logger.should be_a(Logger)
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "configuration" do
|
35
|
+
|
36
|
+
it "should return the configuration hash" do
|
37
|
+
Voorhees::Config.configuration.should == {:one => 1, :two => 2}
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
|
42
|
+
describe "[]" do
|
43
|
+
|
44
|
+
it "should return the config option matching the key" do
|
45
|
+
Voorhees::Config[:one].should == 1
|
46
|
+
end
|
47
|
+
|
48
|
+
it "should return nil if the key doesn't exist" do
|
49
|
+
Voorhees::Config[:monkey].should be_nil
|
50
|
+
end
|
51
|
+
|
52
|
+
end
|
53
|
+
|
54
|
+
describe "[]=" do
|
55
|
+
|
56
|
+
it "should set the config option for the key" do
|
57
|
+
lambda{
|
58
|
+
Voorhees::Config[:banana] = :yellow
|
59
|
+
}.should change(Voorhees::Config, :banana).from(nil).to(:yellow)
|
60
|
+
end
|
61
|
+
|
62
|
+
end
|
63
|
+
|
64
|
+
describe "delete" do
|
65
|
+
|
66
|
+
it "should delete the config option for the key" do
|
67
|
+
lambda{
|
68
|
+
Voorhees::Config.delete(:one)
|
69
|
+
}.should change(Voorhees::Config, :one).from(1).to(nil)
|
70
|
+
end
|
71
|
+
|
72
|
+
it "should leave the config the same if the key doesn't exist" do
|
73
|
+
lambda{
|
74
|
+
Voorhees::Config.delete(:test)
|
75
|
+
}.should_not change(Voorhees::Config, :configuration)
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
|
80
|
+
describe "fetch" do
|
81
|
+
|
82
|
+
it "should return the config option matching the key if it exists" do
|
83
|
+
Voorhees::Config.fetch(:one, 100).should == 1
|
84
|
+
end
|
85
|
+
|
86
|
+
it "should return the config default if the key doesn't exist" do
|
87
|
+
Voorhees::Config.fetch(:other, 100).should == 100
|
88
|
+
end
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
describe "to_hash" do
|
93
|
+
|
94
|
+
it "should return a hash of the configuration" do
|
95
|
+
Voorhees::Config.to_hash.should == {:one => 1, :two => 2}
|
96
|
+
end
|
97
|
+
|
98
|
+
end
|
99
|
+
|
100
|
+
describe "setup" do
|
101
|
+
|
102
|
+
it "should yield self" do
|
103
|
+
Voorhees::Config.setup do |c|
|
104
|
+
c.should == Voorhees::Config
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
it "should let you set items on the configuration object as a hash" do
|
109
|
+
lambda{
|
110
|
+
Voorhees::Config.setup do |c|
|
111
|
+
c[:bananas] = 100
|
112
|
+
end
|
113
|
+
}.should change(Voorhees::Config, :bananas).from(nil).to(100)
|
114
|
+
end
|
115
|
+
|
116
|
+
it "should let you set items on the configuration object as a method" do
|
117
|
+
lambda{
|
118
|
+
Voorhees::Config.setup do |c|
|
119
|
+
c.monkeys = 100
|
120
|
+
end
|
121
|
+
}.should change(Voorhees::Config, :monkeys).from(nil).to(100)
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
describe "calling a missing method" do
|
127
|
+
|
128
|
+
it "should retreive the config if the method matches a key" do
|
129
|
+
Voorhees::Config.one.should == 1
|
130
|
+
end
|
131
|
+
|
132
|
+
it "should retreive nil if the method doesn't match a key" do
|
133
|
+
Voorhees::Config.moo.should be_nil
|
134
|
+
end
|
135
|
+
|
136
|
+
it "should set the value of the config item matching the method name if it's an assignment" do
|
137
|
+
lambda{
|
138
|
+
Voorhees::Config.trees = 3
|
139
|
+
}.should change(Voorhees::Config, :trees).from(nil).to(3)
|
140
|
+
end
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|