voorhees 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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