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