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.
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
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.
@@ -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.
@@ -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
@@ -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
+
@@ -0,0 +1,6 @@
1
+ require "voorhees/logging"
2
+ require "voorhees/config"
3
+ require "voorhees/exceptions"
4
+ require "voorhees/request"
5
+ require "voorhees/response"
6
+ require "voorhees/resource"
@@ -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