rested 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ *.swo
2
+ *.swp
3
+ pkg
data/README.rdoc ADDED
@@ -0,0 +1,96 @@
1
+ = rested
2
+
3
+ * http://github.com/chetan/rested
4
+
5
+ == DESCRIPTION:
6
+
7
+ Ruby library built on top of httpclient for working with RESTful APIs.
8
+
9
+ == DOCUMENTATION:
10
+
11
+ Documentation is available online at at rdoc.info[http://rdoc.info/projects/chetan/rested]
12
+
13
+ == FEATURES:
14
+
15
+ == SYNOPSIS:
16
+
17
+ class Foo < Rested::Entity
18
+ base_url "http://api.example.com/entities/"
19
+ endpoint "foo"
20
+ user "user@example.com"
21
+ pass "foobar"
22
+
23
+ field :id, :baz, :bar
24
+ end
25
+
26
+ foo = Foo.find(1)
27
+ foo.baz = "frobnicator"
28
+ foo.save!
29
+
30
+ moe = Foo.new
31
+ moe.bar = "curly"
32
+ moe.save!
33
+ puts "new id is #{moe.id}"
34
+
35
+ # to upload a file
36
+ moe.add_file("param_name", "/path/to/file")
37
+ moe.save!
38
+
39
+ # non-entity based example
40
+
41
+ class Baz < Rested::Base
42
+ base_url "http://api.example.com/methods/"
43
+ user "user@example.com"
44
+ pass "foobar"
45
+
46
+ def self.search(terms)
47
+ params = {}
48
+ params["terms"] = terms
49
+ ret = post("/search", params)
50
+ ret["search_results"]
51
+ end
52
+
53
+ end
54
+
55
+
56
+ == REQUIREMENTS:
57
+
58
+ * httpclient
59
+ * json
60
+
61
+ == INSTALL:
62
+
63
+ With gemcutter:
64
+
65
+ sudo gem install rested
66
+
67
+ Without gemcutter:
68
+
69
+ git clone git://github.com/chetan/rested.git
70
+ cd rested
71
+ sudo rake install
72
+
73
+ == LICENSE:
74
+
75
+ (The MIT License)
76
+
77
+ Copyright (c) 2010 Better Advertising, Inc.
78
+
79
+ Permission is hereby granted, free of charge, to any person obtaining
80
+ a copy of this software and associated documentation files (the
81
+ 'Software'), to deal in the Software without restriction, including
82
+ without limitation the rights to use, copy, modify, merge, publish,
83
+ distribute, sublicense, and/or sell copies of the Software, and to
84
+ permit persons to whom the Software is furnished to do so, subject to
85
+ the following conditions:
86
+
87
+ The above copyright notice and this permission notice shall be
88
+ included in all copies or substantial portions of the Software.
89
+
90
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
91
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
92
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
93
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
94
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
95
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
96
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,28 @@
1
+
2
+ begin
3
+ require 'rubygems'
4
+ require 'jeweler'
5
+ Jeweler::Tasks.new do |gemspec|
6
+ gemspec.name = "rested"
7
+ gemspec.summary = "Ruby library for working with RESTful APIs"
8
+ gemspec.description = "Ruby library built on top of httpclient for working with RESTful APIs."
9
+ gemspec.email = "chetan@betteradvertising.com"
10
+ gemspec.homepage = ""
11
+ gemspec.authors = ["Chetan Sarva"]
12
+ gemspec.add_dependency('httpclient', '>= 2.1.5.2')
13
+ gemspec.add_dependency('json', '>= 1.4.2')
14
+ end
15
+ Jeweler::GemcutterTasks.new
16
+ rescue LoadError
17
+ puts "Jeweler not available. Install it with: sudo gem install jeweler"
18
+ end
19
+
20
+ require "rake/testtask"
21
+ desc "Run unit tests"
22
+ Rake::TestTask.new("test") { |t|
23
+ #t.libs << "test"
24
+ t.ruby_opts << "-rubygems"
25
+ t.pattern = "test/**/*_test.rb"
26
+ t.verbose = false
27
+ t.warning = false
28
+ }
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.0
@@ -0,0 +1,97 @@
1
+
2
+ require 'rubygems'
3
+ require 'httpclient'
4
+ require 'json'
5
+
6
+ module Rested
7
+
8
+ module BaseMethods
9
+
10
+ # override this method to customize HTTPClient
11
+ def setup_client
12
+ @client = ::HTTPClient.new
13
+ if self.user and self.pass then
14
+ @client.set_auth(self.base_url, self.user, self.pass)
15
+ end
16
+ @client.debug_dev = STDOUT if Rested.debug
17
+ @client
18
+ end
19
+
20
+ def client
21
+ @client ||= setup_client()
22
+ end
23
+
24
+ def get(uri, params = nil)
25
+ url = self.base_url + uri
26
+ Rested.log_out{"GET #{url}"}
27
+ Rested.log_out{" params: " + params.inspect}
28
+ handle_response(self.client.get(url, params))
29
+ end
30
+
31
+ def post(uri, params = nil)
32
+ url = self.base_url + uri
33
+ Rested.log_out{"POST #{url}"}
34
+ Rested.log_out{" params: " + params.inspect}
35
+ handle_response(self.client.post(url, params))
36
+ end
37
+
38
+ def delete(uri)
39
+ url = self.base_url + uri
40
+ Rested.log_out{"DELETE #{url}"}
41
+ res = self.client.delete(url)
42
+ handle_error(res) if message.status >= 400
43
+ return res.status == 200
44
+ end
45
+
46
+ def handle_response(message)
47
+ handle_error(message) if message.status >= 400
48
+ Rested.log_in{ puts; "HTTP/#{message.version} #{message.status} #{message.reason}"}
49
+ Rested.log_in{ message.header.all.each{|h| Rested.log_in("#{h[0]}: #{h[1]}")}; puts }
50
+ decode_response(message)
51
+ end
52
+
53
+ def decode_response(message)
54
+ ct = message.contenttype
55
+ if ct =~ /json|javascript/ then
56
+ decode_json_response(message)
57
+ elsif ct =~ /xml/ then
58
+ decode_xml_response(message)
59
+ else
60
+ Rested.log_in { "\n" + message.content }
61
+ raise "Unknown response type"
62
+ end
63
+ end
64
+
65
+ def decode_json_response(message)
66
+ begin
67
+ JSON.load(message.content)
68
+ rescue => ex
69
+ nil
70
+ end
71
+ end
72
+
73
+ def decode_xml_response(message)
74
+ raise NotImplementedError # TODO
75
+ end
76
+
77
+ def handle_error(message)
78
+ Rested.log_in{ puts; "HTTP/#{message.version} #{message.status} #{message.reason}"}
79
+ Rested.log_in{ message.header.all.each{|h| Rested.log_in("#{h[0]}: #{h[1]}")}; puts }
80
+ raise Rested::Error.new(message)
81
+ end
82
+
83
+ end
84
+
85
+ class Base
86
+
87
+ rattr_accessor :base_url, :user, :pass
88
+
89
+ class << self
90
+ include BaseMethods
91
+ end
92
+
93
+ include BaseMethods
94
+
95
+ end
96
+
97
+ end
@@ -0,0 +1,59 @@
1
+ module Rested
2
+
3
+ @debug = false
4
+
5
+ def self.debug(val=nil)
6
+ return @debug unless val
7
+ if val == true then
8
+ @debug = STDOUT
9
+ else
10
+ @debug = val
11
+ end
12
+ end
13
+
14
+ def self.debug=(val)
15
+ self.debug(val)
16
+ end
17
+
18
+ def self.log(msg = nil, &block)
19
+ return unless @debug
20
+ if block_given? then
21
+ s = yield
22
+ return if not s.kind_of? String
23
+ @debug.puts(s)
24
+ elsif not msg.nil? then
25
+ @debug.puts(msg)
26
+ end
27
+ end
28
+
29
+ def self.log_do(msg = nil, &block)
30
+ if block_given? then
31
+ s = yield
32
+ return if not s.kind_of? String
33
+ log { "* " + s }
34
+ else
35
+ log("* #{msg}")
36
+ end
37
+ end
38
+
39
+ def self.log_in(msg = nil, &block)
40
+ if block_given? then
41
+ s = yield
42
+ return if not s.kind_of? String
43
+ log { "< " + s }
44
+ else
45
+ log("< #{msg}")
46
+ end
47
+ end
48
+
49
+ def self.log_out(msg = nil, &block)
50
+ if block_given? then
51
+ s = yield
52
+ return if not s.kind_of? String
53
+ log { "> " + s }
54
+ else
55
+ log("> #{msg}")
56
+ end
57
+ end
58
+
59
+ end
@@ -0,0 +1,247 @@
1
+ require File.dirname(__FILE__) + '/ext'
2
+ require File.dirname(__FILE__) + '/base'
3
+
4
+ module Rested
5
+
6
+ class Entity < Base
7
+
8
+ rattr_accessor :endpoint, :id_field
9
+ attr_accessor :errors
10
+ Entity.id_field(:id)
11
+
12
+ class << self
13
+
14
+ attr_reader :before_filters, :after_filters
15
+
16
+ def before_filters
17
+ @before_filters ||= []
18
+ end
19
+
20
+ def after_filters
21
+ @after_filters ||= []
22
+ end
23
+
24
+ def before_save(&block)
25
+ before_filters << block
26
+ end
27
+
28
+ def after_save(&block)
29
+ after_filters << block
30
+ end
31
+
32
+ def id_field
33
+ @id_field ||= inherit_static_from_super(:id_field) || nil
34
+ end
35
+
36
+ def inherit_static_from_super(sym)
37
+ if superclass.respond_to? sym then
38
+ val = superclass.send(sym)
39
+ return val if val.kind_of? Symbol
40
+ return val.dup if not val.nil?
41
+ end
42
+ return nil
43
+ end
44
+
45
+ def fields
46
+ @fields ||= inherit_static_from_super(:fields) || []
47
+ end
48
+
49
+ def field(*args)
50
+ args.each do |f|
51
+ add_field(f) unless fields.include? f
52
+ end
53
+ end
54
+
55
+ def delimited_fields
56
+ @delimited_fields ||= {}
57
+ end
58
+
59
+ def delimited_field(field, delimiter = ',')
60
+ unless fields.include? field
61
+ delimited_fields[field] = delimiter
62
+ add_field(field)
63
+ end
64
+ end
65
+
66
+ def add_field(field)
67
+ self.fields << field
68
+ attr_accessor field
69
+ end
70
+
71
+ def find(id = nil, masquerade = nil)
72
+ uri = self.endpoint
73
+ uri += "/#{id}" if not id.nil?
74
+ begin
75
+ json = get(uri, :masquerade => masquerade)
76
+ rescue Rested::Error => ex
77
+ if ex.message =~ /Invalid/ then
78
+ raise ObjectNotFound.new(ex.http_response)
79
+ end
80
+ end
81
+ if id.nil? then
82
+ # return as list
83
+ return json.values.first.map { |j| new(j) }
84
+ end
85
+ return nil if json.values.empty?
86
+ return new(json.values.first)
87
+ end
88
+
89
+ def list
90
+ find()
91
+ end
92
+
93
+ end
94
+
95
+ def files
96
+ @files ||= {}
97
+ end
98
+
99
+ def add_file(name, file)
100
+ if file.kind_of? String then
101
+ raise IOError.new("File not found: #{file}") if not File.exists? file
102
+ file = File.new(file)
103
+ elsif file.is_a? Tempfile
104
+ file = File.new(file.path)
105
+ end
106
+ return if not file.kind_of? File
107
+ self.files[name] = file
108
+ end
109
+
110
+ def initialize(*args)
111
+ if args.kind_of? Hash then
112
+ h = args
113
+ elsif args.kind_of? Array and args.first.kind_of? Hash then
114
+ h = args.first
115
+ end
116
+ set_values(h) if h
117
+ self.errors = []
118
+ end
119
+
120
+ def set_values(h)
121
+ h.each_pair do |name, value|
122
+ writer_method = "#{name}="
123
+ value = parse_value(name, value)
124
+ if respond_to?(writer_method)
125
+ send(writer_method, value)
126
+ else
127
+ self[name.to_s] = value
128
+ end
129
+ end
130
+ end
131
+
132
+ def parse_value(name, value)
133
+ if !value.nil? then
134
+ if delimited_fields.include?(name.to_sym) then
135
+ value = value.split(delimited_fields[name.to_sym]) if value.is_a?(String)
136
+ value = value.map(&:to_i) if value.first.is_a?(String) && value.all?{ |v| v.to_i.to_s == v }
137
+ else
138
+ value = value.to_i if value.is_a?(String) && value.to_i.to_s == value
139
+ end
140
+ value
141
+ end
142
+ end
143
+
144
+ def id_val
145
+ self.send(id_field)
146
+ end
147
+
148
+ def id_val=(val)
149
+ self.send("#{id_field.to_s}=", val)
150
+ end
151
+
152
+ def new_record?
153
+ self.new?
154
+ end
155
+
156
+ def new?
157
+ self.id_val.nil?
158
+ end
159
+
160
+ def [](name)
161
+ begin
162
+ send(name)
163
+ rescue NoMethodError
164
+ nil
165
+ end
166
+ end
167
+
168
+ def []=(name, value)
169
+ begin
170
+ send("#{name}=", value)
171
+ rescue NoMethodError
172
+ self.class.add_field(name)
173
+ send("#{name}=", value)
174
+ end
175
+ end
176
+
177
+ def to_h
178
+ h = {}
179
+ fields.each do |f|
180
+ val = self.send(f)
181
+ h[f] = val && delimited_fields.include?(f) ? val.join(delimited_fields[f]) : val
182
+ end
183
+ h
184
+ end
185
+
186
+ def to_json
187
+ JSON.generate(to_h())
188
+ end
189
+
190
+ def to_s
191
+ to_json()
192
+ end
193
+
194
+ def update_attributes(attributes = {})
195
+ attributes.each_pair do |field, value|
196
+ send("#{field}=", value)
197
+ end
198
+ save
199
+ end
200
+
201
+ def save
202
+ begin
203
+ save!
204
+ rescue Rested::Error => e
205
+ self.errors = e.validations
206
+ false
207
+ end
208
+ end
209
+
210
+ def save!(params = nil)
211
+ self.class.before_filters.each do |f|
212
+ f.call(self)
213
+ end
214
+ uri = self.endpoint
215
+ uri += "/#{self.id_val}" unless new?
216
+ params = to_h() if not params
217
+ params.delete(self.id_field) if new?
218
+ unless self.files.empty?
219
+ params.merge!(self.files)
220
+ @files = {}
221
+ end
222
+ ret = self.post(uri, params)
223
+ if new? then
224
+ self.id_val = self.class.new(ret.values.first).id_val
225
+ end
226
+ self.class.after_filters.each do |f|
227
+ f.call(self)
228
+ end
229
+ true
230
+ end
231
+
232
+ def delete!
233
+ return if new?
234
+ uri = self.endpoint + "/#{self.id_val}"
235
+ self.delete(uri)
236
+ true
237
+ end
238
+
239
+ def fields
240
+ @fields ||= self.class.fields
241
+ end
242
+
243
+ def delimited_fields
244
+ @delimited_fields ||= self.class.delimited_fields
245
+ end
246
+ end
247
+ end
@@ -0,0 +1,57 @@
1
+
2
+ module Rested
3
+
4
+ class Error < ::Exception
5
+
6
+ attr_accessor :status, :reason, :http_response, :message, :validations
7
+
8
+ def initialize(res)
9
+
10
+ hash = Rested::Base.decode_response(res)
11
+ if hash.include? "error" then
12
+ self.message = hash["error"]
13
+ end
14
+ self.validations = Validations.new(hash['validations'])
15
+ self.status = res.status
16
+ self.reason = res.reason
17
+ self.http_response = res
18
+
19
+ end
20
+
21
+ def to_s
22
+ "#{self.status} #{self.reason}" + (self.message.nil? ? "" : ": #{self.message}")
23
+ end
24
+
25
+ # def self.handle(res)
26
+ # case res.status
27
+ # when 400
28
+ # raise "Bad request"
29
+ # when 401
30
+ # raise "401 Unauthorized"
31
+ # when 403
32
+ # raise "403 Forbidden"
33
+ # when 404
34
+ # raise "404 Not Found"
35
+ # when 405
36
+ # raise "405 Method Not Allowed"
37
+ # else
38
+ # raise "#{res.status} #{res.reason}" if res.status >= 400
39
+ # end
40
+ # end
41
+
42
+ end
43
+
44
+ class ObjectNotFound < Error
45
+ attr_accessor :id
46
+
47
+ def initialize(res)
48
+ super(res)
49
+ self.id = $1 if res.content =~ /Invalid Object.*?(\d+)/
50
+ end
51
+
52
+ def to_s
53
+ "Invalid Object ID '#{self.id}'"
54
+ end
55
+ end
56
+
57
+ end
data/lib/rested/ext.rb ADDED
@@ -0,0 +1,97 @@
1
+
2
+ # orginally cribbed from Rails edge. modified a bunch.
3
+
4
+ class Hash
5
+ # By default, only instances of Hash itself are extractable.
6
+ # Subclasses of Hash may implement this method and return
7
+ # true to declare themselves as extractable. If a Hash
8
+ # is extractable, Array#extract_options! pops it from
9
+ # the Array when it is the last element of the Array.
10
+ def extractable_options?
11
+ instance_of?(Hash)
12
+ end
13
+ end if not {}.respond_to? :extractable_options?
14
+
15
+ class Array
16
+ # Extracts options from a set of arguments. Removes and returns the last
17
+ # element in the array if it's a hash, otherwise returns a blank hash.
18
+ #
19
+ # def options(*args)
20
+ # args.extract_options!
21
+ # end
22
+ #
23
+ # options(1, 2) # => {}
24
+ # options(1, 2, :a => :b) # => {:a=>:b}
25
+ def extract_options!
26
+ if last.is_a?(Hash) && last.extractable_options?
27
+ pop
28
+ else
29
+ {}
30
+ end
31
+ end
32
+ end if not [].respond_to? :extract_options!
33
+
34
+ # Extends the class object with class and instance accessors for attributes,
35
+ # just like the native attr* accessors for instance attributes. Attributes can be
36
+ # set at the class level and overriden at the instance level as well.
37
+ #
38
+ # class Person
39
+ # cattr_accessor :hair_colors
40
+ # end
41
+ #
42
+ # Person.hair_colors = [:brown, :black, :blonde, :red]
43
+ class Class
44
+ def rattr_reader(*syms)
45
+ options = syms.extract_options!
46
+ syms.each do |sym|
47
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
48
+ unless defined? @#{sym}
49
+ @#{sym} = nil
50
+ end
51
+
52
+ def self.#{sym}(#{sym} = nil)
53
+ return @#{sym} unless #{sym}
54
+ @#{sym} = #{sym}
55
+ end
56
+ EOS
57
+
58
+ unless options[:instance_reader] == false
59
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
60
+ def #{sym}
61
+ return @#{sym} if @#{sym}
62
+ self.class.#{sym}
63
+ end
64
+ EOS
65
+ end
66
+ end
67
+ end
68
+
69
+ def rattr_writer(*syms)
70
+ options = syms.extract_options!
71
+ syms.each do |sym|
72
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
73
+ unless defined? @#{sym}
74
+ @#{sym} = nil
75
+ end
76
+
77
+ def self.#{sym}=(obj)
78
+ @#{sym} = obj
79
+ end
80
+ EOS
81
+
82
+ unless options[:instance_writer] == false
83
+ class_eval(<<-EOS, __FILE__, __LINE__ + 1)
84
+ def #{sym}=(obj)
85
+ @#{sym} = obj
86
+ end
87
+ EOS
88
+ end
89
+ self.send("#{sym}=", yield) if block_given?
90
+ end
91
+ end
92
+
93
+ def rattr_accessor(*syms, &blk)
94
+ rattr_reader(*syms)
95
+ rattr_writer(*syms, &blk)
96
+ end
97
+ end if not Class.respond_to? :rattr_accessor
@@ -0,0 +1,25 @@
1
+ module Rested
2
+ class Validations
3
+ def initialize(validations_hash)
4
+ self.validations_hash = validations_hash || {}
5
+ end
6
+
7
+ def full_messages
8
+ validations_hash.inject([]) do |messages, validation|
9
+ messages << "#{humanize(validation.first)} #{validation.last}"
10
+ end
11
+ end
12
+
13
+ def count
14
+ validations_hash.size
15
+ end
16
+
17
+ private
18
+
19
+ def humanize(field)
20
+ field.gsub(/[A-Z]/){ " #{$&.downcase}"}.capitalize
21
+ end
22
+
23
+ attr_accessor :validations_hash
24
+ end
25
+ end
data/lib/rested.rb ADDED
@@ -0,0 +1,7 @@
1
+
2
+ require File.dirname(__FILE__) + '/rested/debug'
3
+ require File.dirname(__FILE__) + '/rested/ext'
4
+ require File.dirname(__FILE__) + '/rested/error'
5
+ require File.dirname(__FILE__) + '/rested/base'
6
+ require File.dirname(__FILE__) + '/rested/entity'
7
+ require File.dirname(__FILE__) + '/rested/validations'
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rested
3
+ version: !ruby/object:Gem::Version
4
+ hash: 23
5
+ prerelease: false
6
+ segments:
7
+ - 0
8
+ - 2
9
+ - 0
10
+ version: 0.2.0
11
+ platform: ruby
12
+ authors:
13
+ - Chetan Sarva
14
+ autorequire:
15
+ bindir: bin
16
+ cert_chain: []
17
+
18
+ date: 2010-07-21 00:00:00 -04:00
19
+ default_executable:
20
+ dependencies:
21
+ - !ruby/object:Gem::Dependency
22
+ name: httpclient
23
+ prerelease: false
24
+ requirement: &id001 !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ">="
28
+ - !ruby/object:Gem::Version
29
+ hash: 119
30
+ segments:
31
+ - 2
32
+ - 1
33
+ - 5
34
+ - 2
35
+ version: 2.1.5.2
36
+ type: :runtime
37
+ version_requirements: *id001
38
+ - !ruby/object:Gem::Dependency
39
+ name: json
40
+ prerelease: false
41
+ requirement: &id002 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ hash: 3
47
+ segments:
48
+ - 1
49
+ - 4
50
+ - 2
51
+ version: 1.4.2
52
+ type: :runtime
53
+ version_requirements: *id002
54
+ description: Ruby library built on top of httpclient for working with RESTful APIs.
55
+ email: chetan@betteradvertising.com
56
+ executables: []
57
+
58
+ extensions: []
59
+
60
+ extra_rdoc_files:
61
+ - README.rdoc
62
+ files:
63
+ - .gitignore
64
+ - README.rdoc
65
+ - Rakefile
66
+ - VERSION
67
+ - lib/rested.rb
68
+ - lib/rested/base.rb
69
+ - lib/rested/debug.rb
70
+ - lib/rested/entity.rb
71
+ - lib/rested/error.rb
72
+ - lib/rested/ext.rb
73
+ - lib/rested/validations.rb
74
+ has_rdoc: true
75
+ homepage: ""
76
+ licenses: []
77
+
78
+ post_install_message:
79
+ rdoc_options:
80
+ - --charset=UTF-8
81
+ require_paths:
82
+ - lib
83
+ required_ruby_version: !ruby/object:Gem::Requirement
84
+ none: false
85
+ requirements:
86
+ - - ">="
87
+ - !ruby/object:Gem::Version
88
+ hash: 3
89
+ segments:
90
+ - 0
91
+ version: "0"
92
+ required_rubygems_version: !ruby/object:Gem::Requirement
93
+ none: false
94
+ requirements:
95
+ - - ">="
96
+ - !ruby/object:Gem::Version
97
+ hash: 3
98
+ segments:
99
+ - 0
100
+ version: "0"
101
+ requirements: []
102
+
103
+ rubyforge_project:
104
+ rubygems_version: 1.3.7
105
+ signing_key:
106
+ specification_version: 3
107
+ summary: Ruby library for working with RESTful APIs
108
+ test_files: []
109
+