voorhees 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
data/LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2009 Richard Livsey
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.markdown
ADDED
@@ -0,0 +1,240 @@
|
|
1
|
+
# Voorhees
|
2
|
+
|
3
|
+
## Design goals
|
4
|
+
|
5
|
+
* Be as fast as possible
|
6
|
+
* Be simple, yet configurable
|
7
|
+
* Include just what you need
|
8
|
+
* Don't stomp on object hierarcy (it's a mixin)
|
9
|
+
* Lazy load only the objects you need, when you need them
|
10
|
+
|
11
|
+
## Example usage
|
12
|
+
|
13
|
+
class User
|
14
|
+
include Voorhees::Resource
|
15
|
+
|
16
|
+
json_service :list, :path => "/users/find.json"
|
17
|
+
|
18
|
+
def messages
|
19
|
+
json_request(:class => Message) do |r|
|
20
|
+
r.path = "/#{self.id}/messages.json"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
users = User.list(:page => 2)
|
26
|
+
|
27
|
+
user = users[0]
|
28
|
+
user.json_attributes => [:id, :login, :email]
|
29
|
+
user.raw_json => {:id => 1, :login => 'test', :email => 'bob@example.com'}
|
30
|
+
user.login => 'test'
|
31
|
+
user.messages => [Message, Message, Message, ...]
|
32
|
+
|
33
|
+
See [/examples/](master/examples/) directory for more.
|
34
|
+
|
35
|
+
## A bit more in-depth
|
36
|
+
|
37
|
+
### Configuration
|
38
|
+
|
39
|
+
Setup global configuration for requests with Voorhees::Config
|
40
|
+
These can all be overridden on individual requests/services
|
41
|
+
|
42
|
+
Voorhees::Config.setup do |c|
|
43
|
+
c[:base_uri] = "http://api.example.com/json"
|
44
|
+
c[:defaults] = {:api_version => 2}
|
45
|
+
c[:timeout] = 10
|
46
|
+
c[:retries] = 3
|
47
|
+
end
|
48
|
+
|
49
|
+
#### Global options
|
50
|
+
|
51
|
+
* logger: set a logger to use for debug messages, defaults to Logger.new(STDOUT) or RAILS_DEFAULT_LOGGER if it's defined
|
52
|
+
|
53
|
+
#### Request global options
|
54
|
+
|
55
|
+
These can be set in the global config and overridden on individual services/requests
|
56
|
+
|
57
|
+
* base_uri: Prepend all paths with this, usually the domain of the service
|
58
|
+
* defaults: A hash of default parameters
|
59
|
+
* http_method: The Net::HTTP method to use. One of Net::HTTP::Get (default), Net::HTTP::Post, Net::HTTP::Put or Net::HTTP::Delete
|
60
|
+
* retries: Number of times to retry if it fails to load data from the service
|
61
|
+
* timeout: Number of seconds to wait for the service to send data
|
62
|
+
|
63
|
+
#### Request specific options
|
64
|
+
|
65
|
+
These cannot be globally set and can only be defined on individual services/requests
|
66
|
+
|
67
|
+
* hierarchy: Define the class hierarchy for nested data - see below for info
|
68
|
+
* parameters: Hash of data to send along with the request, overrides any defaults
|
69
|
+
* path: Path to the service. Can be relative if you have a base_uri set.
|
70
|
+
* required: Array of required parameters. Raises a Voorhees::ParameterMissingError if a required parameter is not set.
|
71
|
+
|
72
|
+
### Timeouts and Retries
|
73
|
+
|
74
|
+
As well as setting the open_timeout/read_timeout of Net::HTTP, we also wrap each request in a timeout check.
|
75
|
+
|
76
|
+
If [SystemTimer](http://ph7spot.com/articles/system_timer) is installed it will use this, otherwise it falls back on the Timeout library.
|
77
|
+
|
78
|
+
If the request fails with a Timeout::Error, or a Errno::ECONNREFUSED, we attept the request again upto the number of retries specified.
|
79
|
+
|
80
|
+
For Errno::ECONNREFUSED errors, we also sleep for 1 second to give the service a chance to wake up.
|
81
|
+
|
82
|
+
### Services and Requests
|
83
|
+
|
84
|
+
There are 3 ways to communicate with the service.
|
85
|
+
|
86
|
+
#### json_service
|
87
|
+
|
88
|
+
This sets up a class method
|
89
|
+
|
90
|
+
class User
|
91
|
+
include Voorhees::Resource
|
92
|
+
json_service :list, :path => "/users.json"
|
93
|
+
end
|
94
|
+
|
95
|
+
User.list(:page => 3) => [User, User, User, ...]
|
96
|
+
|
97
|
+
By default it assumes you're getting items of the same class, you can override this like so:
|
98
|
+
|
99
|
+
json_service :list, :path => "/users.json",
|
100
|
+
:class => OtherClass
|
101
|
+
|
102
|
+
#### json_request
|
103
|
+
|
104
|
+
This is used in instance methods:
|
105
|
+
|
106
|
+
class User
|
107
|
+
include Voorhees::Resource
|
108
|
+
|
109
|
+
def friends
|
110
|
+
json_request do |r|
|
111
|
+
r.path => "/friends.json"
|
112
|
+
r.parameters => {:user_id => self.id}
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
User.new.friends(:limit => 2) => [User, User]
|
118
|
+
|
119
|
+
Like json_service, by default it assumes you're getting items of the same class, you can override this like so:
|
120
|
+
|
121
|
+
def messages
|
122
|
+
json_request(:class => Message) do |r|
|
123
|
+
r.path = "/messages.json"
|
124
|
+
r.parameters = {:user_id => self.id}
|
125
|
+
end
|
126
|
+
end
|
127
|
+
|
128
|
+
User.new.messages => [Message, Message, ...]
|
129
|
+
|
130
|
+
By default a json_request call will convert the JSON to objects, you can make it return something else by setting the :returning property like so:
|
131
|
+
|
132
|
+
json_request(:returning => :raw) do |r|
|
133
|
+
...
|
134
|
+
end
|
135
|
+
|
136
|
+
The returning property can be set to the following:
|
137
|
+
|
138
|
+
* :raw => the raw JSON response as a string
|
139
|
+
* :json => the JSON parsed to a ruby hash (through JSON.parse)
|
140
|
+
* :response => the Voorhees::Response object
|
141
|
+
* :objects => casts the JSON into objects (the default)
|
142
|
+
|
143
|
+
#### Voorhees::Request
|
144
|
+
|
145
|
+
Both json_service and json_request create Voorhees::Request objects to do their bidding.
|
146
|
+
|
147
|
+
If you like you can use this yourself directly.
|
148
|
+
|
149
|
+
This sets up a request identical to the json_request messages example above:
|
150
|
+
|
151
|
+
request = Voorhees::Request.new(Message)
|
152
|
+
request.path = "/messages.json"
|
153
|
+
request.parameters = {:user_id => self.id}
|
154
|
+
|
155
|
+
To perform the HTTP request (returning a Voorhees::Response object):
|
156
|
+
|
157
|
+
response = request.perform
|
158
|
+
|
159
|
+
You can now get at the parsed JSON, or convert them to objects:
|
160
|
+
|
161
|
+
response.json => [{id: 5, subject: "Test", ... }, ...]
|
162
|
+
response.to_objects => [Message, Message, Message, ...]
|
163
|
+
|
164
|
+
### Object Hierarchies
|
165
|
+
|
166
|
+
Say you have a service which responds with a list of users in the following format:
|
167
|
+
|
168
|
+
curl http://example.com/users.json
|
169
|
+
|
170
|
+
[{
|
171
|
+
"email":"bt@example.com",
|
172
|
+
"username":"btables",
|
173
|
+
"name":"Bobby Tables",
|
174
|
+
"id":1,
|
175
|
+
"address":{
|
176
|
+
"street":"24 Monkey Close",
|
177
|
+
"city":"Somesville",
|
178
|
+
"country":"Somewhere",
|
179
|
+
"coords":{
|
180
|
+
"lat":52.9876,
|
181
|
+
"lon":12.3456
|
182
|
+
}
|
183
|
+
}
|
184
|
+
}]
|
185
|
+
|
186
|
+
You can define a service to consume this as follows:
|
187
|
+
|
188
|
+
class User
|
189
|
+
include Voorhees::Resource
|
190
|
+
json_service :list, :path => "http://example.com/users.json"
|
191
|
+
end
|
192
|
+
|
193
|
+
Calling User.list will return a list of User instances.
|
194
|
+
|
195
|
+
users = User.list
|
196
|
+
users[0].name => "bt@example.com"
|
197
|
+
|
198
|
+
However, what about the address? It just returns as a Hash of parsed JSON:
|
199
|
+
|
200
|
+
users[0].address => {"street":"24 Monkey Close", "city":... }
|
201
|
+
|
202
|
+
If you have an Address class you'd like to use, you can tell it like so:
|
203
|
+
|
204
|
+
json_service :list, :path => "http://example.com/users.json",
|
205
|
+
:hierarchy => {:address => Address}
|
206
|
+
|
207
|
+
You can nest hierarchies to an infinite depth like so:
|
208
|
+
|
209
|
+
json_service :list, :path => "http://example.com/users.json",
|
210
|
+
:hierarchy => {:address => [Address, {:coords => LatLon}]}
|
211
|
+
|
212
|
+
Instead of the class name, you can also just use a symbol:
|
213
|
+
|
214
|
+
json_service :list, :path => "http://example.com/users.json",
|
215
|
+
:hierarchy => {:address => [:address, {:coords => :lat_lon}]}
|
216
|
+
|
217
|
+
With that we can now do:
|
218
|
+
|
219
|
+
users = User.list
|
220
|
+
users[0].name => "Bobby Tables"
|
221
|
+
users[0].address.country => "Somewhere"
|
222
|
+
users[0].address.coords.lat => 52.9876
|
223
|
+
|
224
|
+
### Requirements
|
225
|
+
|
226
|
+
* A JSON library which supports JSON.parse
|
227
|
+
* ActiveSupport
|
228
|
+
* SystemTimer - falls back on Timer if it's not available
|
229
|
+
|
230
|
+
## Thanks
|
231
|
+
|
232
|
+
The ideas and design came from discussions when refactoring [LVS::JSONService](http://github.com/LVS/JSONService) the original of which was
|
233
|
+
developed by [Andy Jeffries](http://github.com/andyjeffries/) for use at LVS
|
234
|
+
|
235
|
+
Much discussion with [John Cinnamond](http://github.com/jcinnamond)
|
236
|
+
and [Jason Lee](http://github.com/jlsync)
|
237
|
+
|
238
|
+
## Copyright
|
239
|
+
|
240
|
+
Copyright (c) 2009 Richard Livsey. See LICENSE for details.
|
data/Rakefile
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'jeweler'
|
6
|
+
Jeweler::Tasks.new do |gem|
|
7
|
+
gem.name = "voorhees"
|
8
|
+
gem.summary = %Q{Library to consume and interract with JSON services}
|
9
|
+
gem.email = "richard@livsey.org"
|
10
|
+
gem.homepage = "http://github.com/rlivsey/voorhees"
|
11
|
+
gem.authors = ["Richard Livsey"]
|
12
|
+
# gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
|
13
|
+
end
|
14
|
+
|
15
|
+
rescue LoadError
|
16
|
+
puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
|
17
|
+
end
|
18
|
+
|
19
|
+
begin
|
20
|
+
gem 'ci_reporter'
|
21
|
+
require 'ci/reporter/rake/rspec'
|
22
|
+
rescue LoadError
|
23
|
+
# do nothing
|
24
|
+
end
|
25
|
+
|
26
|
+
require 'spec/rake/spectask'
|
27
|
+
Spec::Rake::SpecTask.new(:spec) do |spec|
|
28
|
+
spec.libs << 'lib' << 'spec'
|
29
|
+
spec.spec_files = FileList['spec/**/*_spec.rb']
|
30
|
+
end
|
31
|
+
|
32
|
+
Spec::Rake::SpecTask.new(:rcov) do |spec|
|
33
|
+
spec.libs << 'lib' << 'spec'
|
34
|
+
spec.pattern = 'spec/**/*_spec.rb'
|
35
|
+
spec.rcov = true
|
36
|
+
end
|
37
|
+
|
38
|
+
|
39
|
+
task :default => :spec
|
40
|
+
|
41
|
+
require 'rake/rdoctask'
|
42
|
+
Rake::RDocTask.new do |rdoc|
|
43
|
+
if File.exist?('VERSION.yml')
|
44
|
+
config = YAML.load(File.read('VERSION.yml'))
|
45
|
+
version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
|
46
|
+
else
|
47
|
+
version = ""
|
48
|
+
end
|
49
|
+
|
50
|
+
rdoc.rdoc_dir = 'rdoc'
|
51
|
+
rdoc.title = "voorhees #{version}"
|
52
|
+
rdoc.rdoc_files.include('README*')
|
53
|
+
rdoc.rdoc_files.include('lib/**/*.rb')
|
54
|
+
end
|
55
|
+
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.2.0
|
data/examples/twitter.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
$LOAD_PATH.unshift(File.dirname(__FILE__))
|
2
|
+
$LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
|
3
|
+
|
4
|
+
require 'rubygems'
|
5
|
+
require 'json' # use whatever JSON gem you want, as long as it supports JSON.parse
|
6
|
+
require 'active_support' # used for inflector
|
7
|
+
require 'voorhees'
|
8
|
+
require 'pp'
|
9
|
+
|
10
|
+
Voorhees::Config.setup do |c|
|
11
|
+
c[:base_uri] = "http://twitter.com"
|
12
|
+
end
|
13
|
+
|
14
|
+
class User
|
15
|
+
include Voorhees::Resource
|
16
|
+
|
17
|
+
json_service :get,
|
18
|
+
:path => "/users/show.json",
|
19
|
+
:hierarchy => {
|
20
|
+
:status => :tweet
|
21
|
+
}
|
22
|
+
|
23
|
+
def friends
|
24
|
+
json_request do |r|
|
25
|
+
r.path = "/statuses/friends.json"
|
26
|
+
r.parameters = {:id => self.screen_name}
|
27
|
+
r.hierarchy = {
|
28
|
+
:status => :tweet
|
29
|
+
}
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
|
35
|
+
class Tweet
|
36
|
+
include Voorhees::Resource
|
37
|
+
|
38
|
+
json_service :public_timeline,
|
39
|
+
:path => "/statuses/public_timeline.json",
|
40
|
+
:hierarchy => {
|
41
|
+
:user => User # can be a class
|
42
|
+
}
|
43
|
+
|
44
|
+
json_service :users_timeline,
|
45
|
+
:path => "/statuses/user_timeline.json",
|
46
|
+
:hierarchy => {
|
47
|
+
:user => :user # or a symbol
|
48
|
+
}
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
puts "> tweets = Tweet.public_timeline"
|
53
|
+
tweets = Tweet.public_timeline
|
54
|
+
|
55
|
+
puts "> tweets[0].class: #{tweets[0].class}"
|
56
|
+
puts "> tweets[0].text: #{tweets[0].text}"
|
57
|
+
puts "> tweets[0].user.name: #{tweets[0].user.name}"
|
58
|
+
|
59
|
+
puts "\n\n"
|
60
|
+
puts "> tweets = Tweet.users_timeline(:id => 'rlivsey', :page => 2)"
|
61
|
+
tweets = Tweet.users_timeline(:id => 'rlivsey', :page => 2)
|
62
|
+
|
63
|
+
puts "> tweets[0].text: #{tweets[0].text}"
|
64
|
+
puts "> tweets[0].user.name: #{tweets[0].user.name}"
|
65
|
+
|
66
|
+
puts "\n\n"
|
67
|
+
|
68
|
+
puts "> rlivsey = User.get(:id => 'rlivsey')"
|
69
|
+
rlivsey = User.get(:id => 'rlivsey')
|
70
|
+
|
71
|
+
puts "> rlivsey.name: #{rlivsey.name}"
|
72
|
+
puts "> rlivsey.location: #{rlivsey.location}"
|
73
|
+
puts "> rlivsey.status.text: #{rlivsey.status.text}"
|
74
|
+
puts "> rlivsey.status.created_at: #{rlivsey.status.created_at}"
|
75
|
+
|
76
|
+
puts "\n\n"
|
77
|
+
|
78
|
+
puts "> friends = rlivsey.friends"
|
79
|
+
friends = rlivsey.friends
|
80
|
+
|
81
|
+
puts "> friends[0].class: #{friends[0].class}"
|
82
|
+
puts "> friends[0].name: #{friends[0].name}"
|
83
|
+
puts "> friends[0].status.text: #{friends[0].status.text}"
|
84
|
+
|
85
|
+
|
data/lib/voorhees.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require 'logger'
|
2
|
+
|
3
|
+
module Voorhees
|
4
|
+
module Config
|
5
|
+
class << self
|
6
|
+
|
7
|
+
# the configuration hash itself
|
8
|
+
def configuration
|
9
|
+
@configuration ||= defaults
|
10
|
+
end
|
11
|
+
|
12
|
+
def defaults
|
13
|
+
{
|
14
|
+
:logger => defined?(RAILS_DEFAULT_LOGGER) ? RAILS_DEFAULT_LOGGER : Logger.new(STDOUT),
|
15
|
+
:timeout => 10,
|
16
|
+
:retries => 0,
|
17
|
+
:http_method => Net::HTTP::Get,
|
18
|
+
:response_class => Voorhees::Response
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def [](key)
|
23
|
+
configuration[key]
|
24
|
+
end
|
25
|
+
|
26
|
+
def []=(key, val)
|
27
|
+
configuration[key] = val
|
28
|
+
end
|
29
|
+
|
30
|
+
# remove an item from the configuration
|
31
|
+
def delete(key)
|
32
|
+
configuration.delete(key)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Return the value of the key, or the default if doesn't exist
|
36
|
+
#
|
37
|
+
# ==== Examples
|
38
|
+
#
|
39
|
+
# Voorhees::Config.fetch(:monkey, false)
|
40
|
+
# => false
|
41
|
+
#
|
42
|
+
def fetch(key, default)
|
43
|
+
configuration.fetch(key, default)
|
44
|
+
end
|
45
|
+
|
46
|
+
def to_hash
|
47
|
+
configuration
|
48
|
+
end
|
49
|
+
|
50
|
+
# Yields the configuration.
|
51
|
+
#
|
52
|
+
# ==== Examples
|
53
|
+
# Voorhees::Config.use do |config|
|
54
|
+
# config[:debug] = true
|
55
|
+
# config.something = false
|
56
|
+
# end
|
57
|
+
#
|
58
|
+
def setup
|
59
|
+
yield self
|
60
|
+
nil
|
61
|
+
end
|
62
|
+
|
63
|
+
def clear
|
64
|
+
@configuration = {}
|
65
|
+
end
|
66
|
+
|
67
|
+
def reset
|
68
|
+
@configuration = defaults
|
69
|
+
end
|
70
|
+
|
71
|
+
# allow getting and setting properties via Voorhees::Config.xxx
|
72
|
+
#
|
73
|
+
# ==== Examples
|
74
|
+
# Voorhees::Config.debug
|
75
|
+
# Voorhees::Config.debug = false
|
76
|
+
#
|
77
|
+
def method_missing(method, *args)
|
78
|
+
if method.to_s[-1,1] == '='
|
79
|
+
configuration[method.to_s.tr('=','').to_sym] = *args
|
80
|
+
else
|
81
|
+
configuration[method]
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|