partigi-partigirb 0.2.1

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 ADDED
@@ -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 Alvaro Bautista & Fernando Blat
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.rdoc ADDED
@@ -0,0 +1,9 @@
1
+ = partigirb
2
+
3
+ Wrapper for the Partigi API.
4
+
5
+ Adapted from Hayes Davis grackle gem (http://github.com/hayesdavis/grackle/tree/master)
6
+
7
+ == Copyright
8
+
9
+ Copyright (c) 2009 Alvaro Bautista & Fernando Blat, released under MIT license
data/Rakefile ADDED
@@ -0,0 +1,56 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "partigirb"
8
+ gem.summary = %Q{TODO}
9
+ gem.email = ["alvarobp@gmail.com", "ferblape@gmail.com"]
10
+ gem.homepage = "http://github.com/partigi/partigirb"
11
+ gem.authors = ["Alvaro Bautista", "Fernando Blat"]
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
+ require 'rake/testtask'
20
+ Rake::TestTask.new(:test) do |test|
21
+ test.libs << 'lib' << 'test'
22
+ test.pattern = 'test/**/*_test.rb'
23
+ test.verbose = true
24
+ end
25
+
26
+ begin
27
+ require 'rcov/rcovtask'
28
+ Rcov::RcovTask.new do |test|
29
+ test.libs << 'test'
30
+ test.pattern = 'test/**/*_test.rb'
31
+ test.verbose = true
32
+ end
33
+ rescue LoadError
34
+ task :rcov do
35
+ abort "RCov is not available. In order to run rcov, you must: sudo gem install spicycode-rcov"
36
+ end
37
+ end
38
+
39
+
40
+ task :default => :test
41
+
42
+ require 'rake/rdoctask'
43
+ Rake::RDocTask.new do |rdoc|
44
+ if File.exist?('VERSION.yml')
45
+ config = YAML.load(File.read('VERSION.yml'))
46
+ version = "#{config[:major]}.#{config[:minor]}.#{config[:patch]}"
47
+ else
48
+ version = ""
49
+ end
50
+
51
+ rdoc.rdoc_dir = 'rdoc'
52
+ rdoc.title = "partigirb #{version}"
53
+ rdoc.rdoc_files.include('README*')
54
+ rdoc.rdoc_files.include('lib/**/*.rb')
55
+ end
56
+
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.2.1
@@ -0,0 +1,190 @@
1
+ module Partigirb
2
+
3
+ class PartigiStruct < OpenStruct
4
+ attr_accessor :id
5
+ end
6
+
7
+ #Raised by methods which call the API if a non-200 response status is received
8
+ class PartigiError < StandardError
9
+ attr_accessor :response_object
10
+ end
11
+
12
+ class Client
13
+ class Request #:nodoc:
14
+ attr_accessor :client, :path, :method, :api_version
15
+
16
+ def initialize(client,api_version=Partigirb::CURRENT_API_VERSION)
17
+ self.client = client
18
+ self.api_version = api_version
19
+ self.method = :get
20
+ self.path = ''
21
+ end
22
+
23
+ def <<(path)
24
+ self.path << path
25
+ end
26
+
27
+ def path?
28
+ path.length > 0
29
+ end
30
+
31
+ def url
32
+ "#{scheme}://#{host}/api/v#{self.api_version}#{path}"
33
+ end
34
+
35
+ def host
36
+ client.api_host
37
+ end
38
+
39
+ def scheme
40
+ 'http'
41
+ end
42
+ end
43
+
44
+ VALID_METHODS = [:get,:post,:put,:delete]
45
+ VALID_FORMATS = [:atom,:xml,:json]
46
+
47
+ PARTIGI_API_HOST = "www.partigi.com"
48
+ TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
49
+
50
+ attr_accessor :default_format, :headers, :api_version, :transport, :request, :api_host, :auth, :handlers
51
+
52
+ def initialize(options={})
53
+ self.transport = Transport.new
54
+ self.api_host = PARTIGI_API_HOST.clone
55
+ self.api_version = options[:api_version] || Partigirb::CURRENT_API_VERSION
56
+ self.headers = {"User-Agent"=>"Partigirb/#{Partigirb::VERSION}"}.merge!(options[:headers]||{})
57
+ self.default_format = options[:default_format] || :atom
58
+ self.handlers = {
59
+ :json => Partigirb::Handlers::JSONHandler.new,
60
+ :xml => Partigirb::Handlers::XMLHandler.new,
61
+ :atom => Partigirb::Handlers::AtomHandler.new,
62
+ :unknown => Partigirb::Handlers::StringHandler.new
63
+ }
64
+ self.handlers.merge!(options[:handlers]||{})
65
+
66
+ # Authentication param should be a hash with keys:
67
+ # login (required)
68
+ # api_secret (required)
69
+ # nonce (optional, would be automatically generated if missing)
70
+ # timestamp (optional, current timestamp will be automatically used if missing)
71
+ self.auth = options[:auth]
72
+ end
73
+
74
+ def method_missing(name,*args)
75
+ # If method is a format name, execute using that format
76
+ if format_invocation?(name)
77
+ return call_with_format(name,*args)
78
+ end
79
+ # If method ends in ! or ? use that to determine post or get
80
+ if name.to_s =~ /^(.*)(!|\?)$/
81
+ name = $1.to_sym
82
+ # ! is a post, ? is a get
83
+ self.request.method = ($2 == '!' ? :post : :get)
84
+ if format_invocation?(name)
85
+ return call_with_format(name,*args)
86
+ else
87
+ self.request << "/#{$1}"
88
+ return call_with_format(self.default_format,*args)
89
+ end
90
+ end
91
+ # Else add to the request path
92
+ self.request << "/#{name}"
93
+ self
94
+ end
95
+
96
+ # Clears any pending request built up by chained methods but not executed
97
+ def clear
98
+ self.request = nil
99
+ end
100
+
101
+ def request
102
+ @request ||= Request.new(self,api_version)
103
+ end
104
+
105
+ protected
106
+
107
+ def call_with_format(format,params={})
108
+ request << ".#{format}"
109
+ res = send_request(params)
110
+ process_response(format,res)
111
+ ensure
112
+ clear
113
+ end
114
+
115
+ def send_request(params)
116
+ begin
117
+ set_authentication_headers
118
+
119
+ transport.request(
120
+ request.method, request.url, :headers=>headers, :params=>params
121
+ )
122
+ rescue => e
123
+ puts e
124
+ end
125
+ end
126
+
127
+ def process_response(format, res)
128
+ fmt_handler = handler(format)
129
+
130
+ begin
131
+ if res.code.to_i != 200
132
+ handle_error_response(res, Partigirb::Handlers::XMLHandler)
133
+ else
134
+ fmt_handler.decode_response(res.body)
135
+ end
136
+ end
137
+ end
138
+
139
+ # TODO: Test for errors
140
+ def handle_error_response(res, handler)
141
+ err = Partigirb::PartigiError.new
142
+ err.response_object = handler.decode_response(res.body)
143
+ raise err
144
+ end
145
+
146
+ def format_invocation?(name)
147
+ self.request.path? && VALID_FORMATS.include?(name)
148
+ end
149
+
150
+ def handler(format)
151
+ handlers[format] || handlers[:unknown]
152
+ end
153
+
154
+ # Adds the proper WSSE headers if there are the right authentication parameters
155
+ def set_authentication_headers
156
+ unless self.auth.nil? || self.auth === Hash || self.auth.empty?
157
+ auths = self.auth.stringify_keys
158
+
159
+ if auths.has_key?('login') && auths.has_key?('api_secret')
160
+ if !auths['timestamp'].nil?
161
+ timestamp = case auths['timestamp']
162
+ when Time
163
+ auths['timestamp'].strftime(TIMESTAMP_FORMAT)
164
+ when String
165
+ auths['timestamp']
166
+ end
167
+ else
168
+ timestamp = Time.now.strftime(TIMESTAMP_FORMAT) if timestamp.nil?
169
+ end
170
+
171
+ nonce = auths['nonce'] || generate_nonce
172
+ password_digest = generate_password_digest(nonce, timestamp, auths['login'], auths['api_secret'])
173
+ headers.merge!({
174
+ 'Authorization' => "WSSE realm=\"#{PARTIGI_API_HOST}\", profile=\"UsernameToken\"",
175
+ 'X-WSSE' => "UsernameToken Username=\"#{auths['login']}\", PasswordDigest=\"#{password_digest}\", Nonce=\"#{nonce}\", Created=\"#{timestamp}\""
176
+ })
177
+ end
178
+ end
179
+ end
180
+
181
+ def generate_nonce
182
+ o = [('a'..'z'),('A'..'Z')].map{|i| i.to_a}.flatten
183
+ Digest::MD5.hexdigest((0..10).map{o[rand(o.length)]}.join)
184
+ end
185
+
186
+ def generate_password_digest(nonce, timestamp, login, secret)
187
+ Base64.encode64(Digest::SHA1.hexdigest("#{nonce}#{timestamp}#{login}#{secret}")).chomp
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,78 @@
1
+ # Ruby Hash extensions from ActiveSupport
2
+
3
+ class Hash
4
+ # Return a new hash with all keys converted to strings.
5
+ def stringify_keys
6
+ inject({}) do |options, (key, value)|
7
+ options[key.to_s] = value
8
+ options
9
+ end
10
+ end
11
+
12
+ # Destructively convert all keys to strings.
13
+ def stringify_keys!
14
+ keys.each do |key|
15
+ self[key.to_s] = delete(key)
16
+ end
17
+ self
18
+ end
19
+ end
20
+
21
+ class Object
22
+ # An object is blank if it's false, empty, or a whitespace string.
23
+ # For example, "", " ", +nil+, [], and {} are blank.
24
+ #
25
+ # This simplifies
26
+ #
27
+ # if !address.nil? && !address.empty?
28
+ #
29
+ # to
30
+ #
31
+ # if !address.blank?
32
+ def blank?
33
+ respond_to?(:empty?) ? empty? : !self
34
+ end
35
+
36
+ # An object is present if it's not blank.
37
+ def present?
38
+ !blank?
39
+ end
40
+ end
41
+
42
+ class NilClass #:nodoc:
43
+ def blank?
44
+ true
45
+ end
46
+ end
47
+
48
+ class FalseClass #:nodoc:
49
+ def blank?
50
+ true
51
+ end
52
+ end
53
+
54
+ class TrueClass #:nodoc:
55
+ def blank?
56
+ false
57
+ end
58
+ end
59
+
60
+ class Array #:nodoc:
61
+ alias_method :blank?, :empty?
62
+ end
63
+
64
+ class Hash #:nodoc:
65
+ alias_method :blank?, :empty?
66
+ end
67
+
68
+ class String #:nodoc:
69
+ def blank?
70
+ self !~ /\S/
71
+ end
72
+ end
73
+
74
+ class Numeric #:nodoc:
75
+ def blank?
76
+ false
77
+ end
78
+ end
@@ -0,0 +1,24 @@
1
+ module Partigirb
2
+ module Handlers
3
+ class AtomHandler < XMLHandler
4
+ def decode_response(body)
5
+ return REXML::Document.new if body.blank?
6
+ xml = REXML::Document.new(body.gsub(/>\s+</,'><'))
7
+
8
+ if xml.root.name == 'feed'
9
+ entries = xml.root.get_elements('entry')
10
+
11
+ # Depending on whether we have one or more entries we return an PartigiStruct or an array of PartigiStruct
12
+ if entries.size == 1
13
+ load_recursive(entries.first)
14
+ else
15
+ entries.map{|e| load_recursive(e)}
16
+ end
17
+ else
18
+ # We just parse as a common XML
19
+ load_recursive(xml.root)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,10 @@
1
+ module Partigirb
2
+ module Handlers
3
+ class JSONHandler
4
+ def decode_response( body )
5
+ # TODO: Implement when API JSON format is ready
6
+ body
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,9 @@
1
+ module Partigirb
2
+ module Handlers
3
+ class StringHandler
4
+ def decode_response( body )
5
+ body
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,51 @@
1
+ module Partigirb
2
+ module Handlers
3
+ class XMLHandler
4
+ def decode_response(body)
5
+ return REXML::Document.new if body.blank?
6
+ xml = REXML::Document.new(body.gsub(/>\s+</,'><'))
7
+ load_recursive(xml.root)
8
+ end
9
+
10
+ private
11
+ def load_recursive(node)
12
+ if array_node?(node)
13
+ node.elements.map {|e| load_recursive(e)}
14
+ elsif node.elements.size > 0
15
+ build_struct(node)
16
+ elsif node.elements.size == 0
17
+ value = node.text
18
+ fixnum?(value) ? value.to_i : value
19
+ end
20
+ end
21
+
22
+ def build_struct(node)
23
+ ts = PartigiStruct.new
24
+ node.elements.each do |e|
25
+ property = ""
26
+
27
+ if !e.namespace.blank?
28
+ ns = e.namespaces.invert[e.namespace]
29
+ property << "#{ns}_" unless ns == 'xmlns'
30
+ end
31
+
32
+ property << e.name
33
+ ts.send("#{property}=", load_recursive(e))
34
+ end
35
+ ts
36
+ end
37
+
38
+ # Most of the time Twitter specifies nodes that contain an array of
39
+ # sub-nodes with a type="array" attribute. There are some nodes that
40
+ # they dont' do that for, though, including the <ids> node returned
41
+ # by the social graph methods. This method tries to work in both situations.
42
+ def array_node?(node)
43
+ node.attributes['type'] == 'collection'
44
+ end
45
+
46
+ def fixnum?(value)
47
+ value =~ /^\d+$/
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,160 @@
1
+ module Partigirb
2
+
3
+ class Response #:nodoc:
4
+ attr_accessor :method, :request_uri, :status, :body
5
+
6
+ def initialize(method,request_uri,status,body)
7
+ self.method = method
8
+ self.request_uri = request_uri
9
+ self.status = status
10
+ self.body = body
11
+ end
12
+ end
13
+
14
+ class Transport
15
+
16
+ attr_accessor :debug
17
+
18
+ CRLF = "\r\n"
19
+
20
+ def req_class(method)
21
+ case method
22
+ when :get then Net::HTTP::Get
23
+ when :post then Net::HTTP::Post
24
+ when :put then Net::HTTP::Put
25
+ when :delete then Net::HTTP::Delete
26
+ end
27
+ end
28
+
29
+ # Options are one of
30
+ # - :params - a hash of parameters to be sent with the request. If a File is a parameter value, \
31
+ # a multipart request will be sent. If a Time is included, .httpdate will be called on it.
32
+ # - :headers - a hash of headers to send with the request
33
+ def request(method, string_url, options={})
34
+ params = stringify_params(options[:params])
35
+ if method == :get && params
36
+ string_url << query_string(params)
37
+ end
38
+ url = URI.parse(string_url)
39
+ begin
40
+ execute_request(method,url,options)
41
+ rescue Timeout::Error
42
+ raise "Timeout while #{method}ing #{url.to_s}"
43
+ end
44
+ end
45
+
46
+ def execute_request(method,url,options={})
47
+ conn = Net::HTTP.new(url.host, url.port)
48
+ #conn.use_ssl = (url.scheme == 'https')
49
+ conn.start do |http|
50
+ req = req_class(method).new(url.request_uri)
51
+
52
+ add_headers(req,options[:headers])
53
+ if file_param?(options[:params])
54
+ add_multipart_data(req,options[:params])
55
+ else
56
+ add_form_data(req,options[:params])
57
+ end
58
+
59
+ dump_request(req) if debug
60
+ res = http.request(req)
61
+ dump_response(res) if debug
62
+ res
63
+ end
64
+ end
65
+
66
+ def query_string(params)
67
+ query = case params
68
+ when Hash then params.map{|key,value| url_encode_param(key,value) }.join("&")
69
+ else url_encode(params.to_s)
70
+ end
71
+ if !(query == nil || query.length == 0) && query[0,1] != '?'
72
+ query = "?#{query}"
73
+ end
74
+ query
75
+ end
76
+
77
+ private
78
+ def stringify_params(params)
79
+ return nil unless params
80
+ params.inject({}) do |h, pair|
81
+ key, value = pair
82
+ if value.respond_to? :httpdate
83
+ value = value.httpdate
84
+ end
85
+ h[key] = value
86
+ h
87
+ end
88
+ end
89
+
90
+ def file_param?(params)
91
+ return false unless params
92
+ params.any? {|key,value| value.respond_to? :read }
93
+ end
94
+
95
+ def url_encode(value)
96
+ require 'cgi' unless defined?(CGI) && defined?(CGI::escape)
97
+ CGI.escape(value.to_s)
98
+ end
99
+
100
+ def url_encode_param(key,value)
101
+ "#{url_encode(key)}=#{url_encode(value)}"
102
+ end
103
+
104
+ def add_headers(req,headers)
105
+ if headers
106
+ headers.each do |header, value|
107
+ req[header] = value
108
+ end
109
+ end
110
+ end
111
+
112
+ def add_form_data(req,params)
113
+ if req.request_body_permitted? && params
114
+ req.set_form_data(params)
115
+ end
116
+ end
117
+
118
+ def add_multipart_data(req,params)
119
+ boundary = Time.now.to_i.to_s(16)
120
+ req["Content-Type"] = "multipart/form-data; boundary=#{boundary}"
121
+ body = ""
122
+ params.each do |key,value|
123
+ esc_key = url_encode(key)
124
+ body << "--#{boundary}#{CRLF}"
125
+ if value.respond_to?(:read)
126
+ mime_type = MIME::Types.type_for(value.path)[0] || MIME::Types["application/octet-stream"][0]
127
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"; filename=\"#{File.basename(value.path)}\"#{CRLF}"
128
+ body << "Content-Type: #{mime_type.simplified}#{CRLF*2}"
129
+ body << value.read
130
+ else
131
+ body << "Content-Disposition: form-data; name=\"#{esc_key}\"#{CRLF*2}#{value}"
132
+ end
133
+ body << CRLF
134
+ end
135
+ body << "--#{boundary}--#{CRLF*2}"
136
+ req.body = body
137
+ req["Content-Length"] = req.body.size
138
+ end
139
+
140
+ private
141
+
142
+ def dump_request(req)
143
+ puts "Sending Request"
144
+ puts"#{req.method} #{req.path}"
145
+ dump_headers(req)
146
+ end
147
+
148
+ def dump_response(res)
149
+ puts "Received Response"
150
+ dump_headers(res)
151
+ puts res.body
152
+ end
153
+
154
+ def dump_headers(msg)
155
+ msg.each_header do |key, value|
156
+ puts "\t#{key}=#{value}"
157
+ end
158
+ end
159
+ end
160
+ end
data/lib/partigirb.rb ADDED
@@ -0,0 +1,25 @@
1
+ module Partigirb
2
+ VERSION='0.1.0'
3
+ CURRENT_API_VERSION=1
4
+ end
5
+
6
+ $:.unshift File.dirname(__FILE__)
7
+
8
+ require 'open-uri'
9
+ require 'net/http'
10
+ require 'base64'
11
+ require 'digest'
12
+ require 'rexml/document'
13
+ require 'mime/types'
14
+ require 'ostruct'
15
+
16
+ require 'partigirb/core_ext'
17
+
18
+ require 'partigirb/handlers/xml_handler'
19
+ require 'partigirb/handlers/atom_handler'
20
+ require 'partigirb/handlers/json_handler'
21
+ require 'partigirb/handlers/string_handler'
22
+
23
+ require 'partigirb/transport'
24
+ require 'partigirb/client'
25
+