partigirb 0.2.7
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/LICENSE +20 -0
- data/README.markdown +110 -0
- data/Rakefile +56 -0
- data/VERSION +1 -0
- data/examples/last_reviews_summary.rb +34 -0
- data/examples/who_ignores_me.rb +35 -0
- data/lib/partigirb/client.rb +190 -0
- data/lib/partigirb/core_ext.rb +78 -0
- data/lib/partigirb/handlers/atom_handler.rb +24 -0
- data/lib/partigirb/handlers/json_handler.rb +10 -0
- data/lib/partigirb/handlers/string_handler.rb +9 -0
- data/lib/partigirb/handlers/xml_handler.rb +111 -0
- data/lib/partigirb/transport.rb +160 -0
- data/lib/partigirb.rb +27 -0
- data/partigirb.gemspec +76 -0
- data/test/atom_handler_test.rb +61 -0
- data/test/client_test.rb +209 -0
- data/test/fixtures/alvaro_friends.atom.xml +80 -0
- data/test/fixtures/pulp_fiction.atom.xml +99 -0
- data/test/json_handler_test.rb +7 -0
- data/test/mocks/net_http_mock.rb +12 -0
- data/test/mocks/response_mock.rb +12 -0
- data/test/mocks/transport_mock.rb +15 -0
- data/test/test_helper.rb +47 -0
- data/test/transport_test.rb +8 -0
- data/test/xml_handler_test.rb +180 -0
- metadata +95 -0
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.markdown
ADDED
@@ -0,0 +1,110 @@
|
|
1
|
+
# partigirb
|
2
|
+
|
3
|
+
A Ruby wrapper for the Partigi API, adapted from [Grackle](http://github.com/hayesdavis/grackle/tree/master) by Hayes Davis.
|
4
|
+
|
5
|
+
## What is Partigi?
|
6
|
+
|
7
|
+
[Partigi](http://www.partigi.com) is a service that helps you choose your next cultural items, share short reviews and keep track of what you have consumed and own.
|
8
|
+
|
9
|
+
The Partigi API is an almost-REST based Atom API where we offer you almost all data and functionality that you have in the website.
|
10
|
+
|
11
|
+
There is also a [complete documentation of the API](http://partigi.pbworks.com/).
|
12
|
+
|
13
|
+
## Install
|
14
|
+
|
15
|
+
gem install partigirb -s http://gemcutter.org
|
16
|
+
|
17
|
+
## Usage
|
18
|
+
|
19
|
+
### Creating a client
|
20
|
+
|
21
|
+
#### Without Authentication
|
22
|
+
|
23
|
+
client = Partigirb::Client.new
|
24
|
+
|
25
|
+
#### With Authentication
|
26
|
+
|
27
|
+
client = Partigirb::Client.new(:auth => {:login => 'SOMEUSERLOGIN', :api_secret => 'SECRETGENERATEDFORTHEUSER'})
|
28
|
+
|
29
|
+
#### With specific API version
|
30
|
+
|
31
|
+
client = Partigirb::Client.new(:api_version => 2)
|
32
|
+
|
33
|
+
### Request methods
|
34
|
+
|
35
|
+
A request to Partigi servers is done by translating Partigi URL paths into a set of chained method calls, just changing slashes by dots. Each call is used to build the request URL until a format call is found which causes the request to be sent. In order to specify the HTTP method use:
|
36
|
+
|
37
|
+
- `?` for HTTP GET
|
38
|
+
- `!` for HTTP POST (in this case you will need to use a client with authentication)
|
39
|
+
|
40
|
+
A request is not performed until either you add the above signs to your last method call or you use a format method call.
|
41
|
+
|
42
|
+
**Note:** Any method call that is not part of a valid API request path will be chained to the request that Partigirb sends to the server, so that when the client finds a format call (method call ending with ? or !) a wrong request will be sent and a PartigiError will be raised. For example:
|
43
|
+
|
44
|
+
client.wrong.items.index?
|
45
|
+
|
46
|
+
or
|
47
|
+
|
48
|
+
client.wrong
|
49
|
+
client.items.index?
|
50
|
+
|
51
|
+
In the second case we do the wrong call and the right one in separated sentences, however the wrong call is chained anyway (in that case the client method `clear` may be used to flush the chain).
|
52
|
+
|
53
|
+
### Formats
|
54
|
+
|
55
|
+
The response format is specified by a method call with the format name (atom, json or xml). Notice that the only format fully implemented on Partigi API at the moment is atom, which is the default used by the wrapper.
|
56
|
+
|
57
|
+
### Example requests
|
58
|
+
|
59
|
+
The simplest way of executing a GET request is to use the `?` notation, using the default format.
|
60
|
+
|
61
|
+
client.users.show? :id => 'johnwayne' # http://www.partigi.com/api/v1/users/show.atom?id=johnwayne
|
62
|
+
|
63
|
+
Also you can force the format:
|
64
|
+
|
65
|
+
client.users.show.json? :id => 'johnwayne' # http://www.partigi.com/api/v1/users/show.json?id=johnwayne
|
66
|
+
|
67
|
+
For POST requests just change `?` by `!`:
|
68
|
+
|
69
|
+
client.reviews.update! :id => 123, :status => 1 # POST to http://www.partigi.com/api/v1/reviews/update.atom
|
70
|
+
|
71
|
+
|
72
|
+
### Parameter handling
|
73
|
+
|
74
|
+
- All parameters are URL encoded as necessary.
|
75
|
+
- If you use a File object as a parameter it will be POSTed to Partigi in a multipart request.
|
76
|
+
- If you use a Time object as a parameter, .httpdate will be called on it and that value will be used
|
77
|
+
|
78
|
+
### Return values
|
79
|
+
|
80
|
+
The returned values are always OpenStruct objects (wrapped in Partigirb::PartigiStruct) containing the response values as attributes.
|
81
|
+
|
82
|
+
If the response contains several entries the client returns an Array of OpenStruct objects.
|
83
|
+
|
84
|
+
When using Atom format Partigi returns some XML elements using namespaces. In those cases the elements are mapped to attributes by convention, for example: `namespaceName:attribute` becomes `namespaceName_attribute`
|
85
|
+
|
86
|
+
#### Special cases
|
87
|
+
|
88
|
+
There are two special cases to be aware of in regard to PartigiStruct:
|
89
|
+
|
90
|
+
- Every attribute which name is equal to any of the Ruby Object methods (e.g `type`) will be mapped to a method on the struct starting with an underscore (e.g `_type`).
|
91
|
+
|
92
|
+
- XML elements that appear repeated with different type values will turn into a unique struct with one method per type. For instance:
|
93
|
+
|
94
|
+
<content type="text">Some text</content>
|
95
|
+
<content type="html"><p>Some html</p></content>
|
96
|
+
|
97
|
+
Will be accessed by `result.content.text` and `result.content.html`, both returning a ruby string.
|
98
|
+
|
99
|
+
### Error handling
|
100
|
+
|
101
|
+
In case Partigi returns an error response, this is turned into a PartigiError object which message attribute is set to the error string returned in the XML response.
|
102
|
+
|
103
|
+
## Requirements
|
104
|
+
|
105
|
+
- json
|
106
|
+
- mime-types
|
107
|
+
|
108
|
+
## Copyright
|
109
|
+
|
110
|
+
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{A Ruby wrapper for the Partigi API}
|
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
|
+
Jeweler::GemcutterTasks.new
|
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.7
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/partigirb'
|
2
|
+
|
3
|
+
if ARGV.empty?
|
4
|
+
puts "\nUsage: ruby #{__FILE__} user_id_or_login\n\n"
|
5
|
+
exit
|
6
|
+
end
|
7
|
+
|
8
|
+
user_id = ARGV.first
|
9
|
+
|
10
|
+
def show_reviews(client, reviews, title)
|
11
|
+
puts
|
12
|
+
puts title
|
13
|
+
puts "-" * title.size
|
14
|
+
puts
|
15
|
+
|
16
|
+
reviews.each do |review|
|
17
|
+
film = client.items.show? :id => review.ptItem_id, :type => 'film'
|
18
|
+
puts "- #{film.title}"
|
19
|
+
puts " Comment: #{review.content.text}"
|
20
|
+
puts
|
21
|
+
end
|
22
|
+
puts
|
23
|
+
end
|
24
|
+
|
25
|
+
client = Partigirb::Client.new
|
26
|
+
|
27
|
+
reviews = client.reviews.index? :user_id => user_id, :per_page => 5, :status => 0, :order => 'desc'
|
28
|
+
show_reviews(client, reviews, "Latest 5 films you want to watch")
|
29
|
+
|
30
|
+
reviews = client.reviews.index? :user_id => user_id, :per_page => 5, :status => 1, :order => 'desc'
|
31
|
+
show_reviews(client, reviews, "Latest 5 films you have seen")
|
32
|
+
|
33
|
+
reviews = client.reviews.index? :user_id => user_id, :per_page => 5, :status => 2, :order => 'desc'
|
34
|
+
show_reviews(client, reviews, "Latest 5 films you own")
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../lib/partigirb'
|
2
|
+
|
3
|
+
if ARGV.empty?
|
4
|
+
puts "\nUsage: #{__FILE__} user_id_or_login\n\n"
|
5
|
+
exit
|
6
|
+
end
|
7
|
+
|
8
|
+
user_id = ARGV.first
|
9
|
+
|
10
|
+
client = Partigirb::Client.new
|
11
|
+
|
12
|
+
traitors = []
|
13
|
+
|
14
|
+
page = 1
|
15
|
+
friends = []
|
16
|
+
|
17
|
+
# Get logins of people user is following
|
18
|
+
begin
|
19
|
+
friends = client.friendships.index? :user_id => user_id, :type => 'follows', :page => page
|
20
|
+
|
21
|
+
friends.each do |friend|
|
22
|
+
relationship = client.friendships.show? :source_id => user_id, :target_id => friend.ptUser_id
|
23
|
+
traitors << friend.ptUser_login if relationship.ptRelationship_source.ptRelationship_followed_by == 'false'
|
24
|
+
end
|
25
|
+
|
26
|
+
page += 1
|
27
|
+
end while !friends.empty?
|
28
|
+
|
29
|
+
if traitors.empty?
|
30
|
+
"Everything is fine. Everyone you follow is following you."
|
31
|
+
else
|
32
|
+
puts "Those are the ones that don't want to know about you:"
|
33
|
+
puts
|
34
|
+
traitors.each {|t| puts " - #{t}"}
|
35
|
+
end
|
@@ -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
|
+
end
|
10
|
+
|
11
|
+
class Client
|
12
|
+
class Request #:nodoc:
|
13
|
+
attr_accessor :client, :path, :method, :api_version
|
14
|
+
|
15
|
+
def initialize(client,api_version=Partigirb::CURRENT_API_VERSION)
|
16
|
+
self.client = client
|
17
|
+
self.api_version = api_version
|
18
|
+
self.method = :get
|
19
|
+
self.path = ''
|
20
|
+
end
|
21
|
+
|
22
|
+
def <<(path)
|
23
|
+
self.path << path
|
24
|
+
end
|
25
|
+
|
26
|
+
def path?
|
27
|
+
path.length > 0
|
28
|
+
end
|
29
|
+
|
30
|
+
def url
|
31
|
+
"#{scheme}://#{host}/api/v#{self.api_version}#{path}"
|
32
|
+
end
|
33
|
+
|
34
|
+
def host
|
35
|
+
client.api_host
|
36
|
+
end
|
37
|
+
|
38
|
+
def scheme
|
39
|
+
'http'
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
VALID_METHODS = [:get,:post,:put,:delete]
|
44
|
+
VALID_FORMATS = [:atom,:xml,:json]
|
45
|
+
|
46
|
+
PARTIGI_API_HOST = "www.partigi.com"
|
47
|
+
TIMESTAMP_FORMAT = '%Y-%m-%dT%H:%M:%SZ'
|
48
|
+
|
49
|
+
attr_accessor :default_format, :headers, :api_version, :transport, :request, :api_host, :auth, :handlers
|
50
|
+
|
51
|
+
def initialize(options={})
|
52
|
+
self.transport = Transport.new
|
53
|
+
self.api_host = PARTIGI_API_HOST.clone
|
54
|
+
self.api_version = options[:api_version] || Partigirb::CURRENT_API_VERSION
|
55
|
+
self.headers = {"User-Agent"=>"Partigirb/#{Partigirb::VERSION}"}.merge!(options[:headers]||{})
|
56
|
+
self.default_format = options[:default_format] || :atom
|
57
|
+
self.handlers = {
|
58
|
+
:json => Partigirb::Handlers::JSONHandler.new,
|
59
|
+
:xml => Partigirb::Handlers::XMLHandler.new,
|
60
|
+
:atom => Partigirb::Handlers::AtomHandler.new,
|
61
|
+
:unknown => Partigirb::Handlers::StringHandler.new
|
62
|
+
}
|
63
|
+
self.handlers.merge!(options[:handlers]||{})
|
64
|
+
|
65
|
+
# Authentication param should be a hash with keys:
|
66
|
+
# login (required)
|
67
|
+
# api_secret (required)
|
68
|
+
# nonce (optional, would be automatically generated if missing)
|
69
|
+
# timestamp (optional, current timestamp will be automatically used if missing)
|
70
|
+
self.auth = options[:auth]
|
71
|
+
end
|
72
|
+
|
73
|
+
def method_missing(name,*args)
|
74
|
+
# If method is a format name, execute using that format
|
75
|
+
if format_invocation?(name)
|
76
|
+
return call_with_format(name,*args)
|
77
|
+
end
|
78
|
+
# If method ends in ! or ? use that to determine post or get
|
79
|
+
if name.to_s =~ /^(.*)(!|\?)$/
|
80
|
+
name = $1.to_sym
|
81
|
+
# ! is a post, ? is a get
|
82
|
+
self.request.method = ($2 == '!' ? :post : :get)
|
83
|
+
if format_invocation?(name)
|
84
|
+
return call_with_format(name,*args)
|
85
|
+
else
|
86
|
+
self.request << "/#{$1}"
|
87
|
+
return call_with_format(self.default_format,*args)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
# Else add to the request path
|
91
|
+
self.request << "/#{name}"
|
92
|
+
self
|
93
|
+
end
|
94
|
+
|
95
|
+
# Clears any pending request built up by chained methods but not executed
|
96
|
+
def clear
|
97
|
+
self.request = nil
|
98
|
+
end
|
99
|
+
|
100
|
+
def request
|
101
|
+
@request ||= Request.new(self,api_version)
|
102
|
+
end
|
103
|
+
|
104
|
+
protected
|
105
|
+
|
106
|
+
def call_with_format(format,params={})
|
107
|
+
request << ".#{format}"
|
108
|
+
res = send_request(params)
|
109
|
+
process_response(format,res)
|
110
|
+
ensure
|
111
|
+
clear
|
112
|
+
end
|
113
|
+
|
114
|
+
def send_request(params)
|
115
|
+
begin
|
116
|
+
set_authentication_headers
|
117
|
+
|
118
|
+
transport.request(
|
119
|
+
request.method, request.url, :headers=>headers, :params=>params
|
120
|
+
)
|
121
|
+
rescue => e
|
122
|
+
puts e
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def process_response(format, res)
|
127
|
+
fmt_handler = handler(format)
|
128
|
+
|
129
|
+
begin
|
130
|
+
if res.code.to_i != 200
|
131
|
+
handle_error_response(res, Partigirb::Handlers::XMLHandler.new)
|
132
|
+
else
|
133
|
+
fmt_handler.decode_response(res.body)
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
# TODO: Test for errors
|
139
|
+
def handle_error_response(res, handler)
|
140
|
+
# Response for errors is an XML document containing an error tag as a root,
|
141
|
+
# having a text node with error name. As XMLHandler starts building the Struct
|
142
|
+
# on root node the returned value from the handler will always be the error name text.
|
143
|
+
raise PartigiError.new(handler.decode_response(res.body))
|
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,111 @@
|
|
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 cdata_node?(node)
|
15
|
+
node.cdatas.first.to_s
|
16
|
+
elsif raw_node?(node)
|
17
|
+
node.text
|
18
|
+
elsif (node.elements.size > 0 || node.attributes.size > 0) && !ignore_attributes?(node)
|
19
|
+
build_struct(node)
|
20
|
+
else
|
21
|
+
value = node.text
|
22
|
+
fixnum?(value) ? value.to_i : value
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def build_struct(node)
|
27
|
+
ts = PartigiStruct.new
|
28
|
+
|
29
|
+
node.attributes.each do |a,v|
|
30
|
+
# In case the Struct object already responds to a method
|
31
|
+
# with same name like type case
|
32
|
+
if ts.respond_to?(a)
|
33
|
+
ts.send("_#{a}=",v)
|
34
|
+
else
|
35
|
+
ts.send("#{a}=", v) unless a =~ /^xmlns/
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
links = node.elements.delete_all('link')
|
40
|
+
|
41
|
+
node.elements.each do |e|
|
42
|
+
property = ""
|
43
|
+
|
44
|
+
if ns = node_namespace(e)
|
45
|
+
property << "#{ns}_" unless ns == 'xmlns'
|
46
|
+
end
|
47
|
+
|
48
|
+
property << e.name
|
49
|
+
|
50
|
+
# Multiple type is the case of content elements, which appear twice, with type="text" and type="html"
|
51
|
+
if multiple_type?(e)
|
52
|
+
if ts.respond_to?(property)
|
53
|
+
ts.send(property).send("#{e.attributes['type']}=", load_recursive(e))
|
54
|
+
else
|
55
|
+
ts.send("#{property}=", PartigiStruct.new)
|
56
|
+
ts.send(property).send("#{e.attributes['type']}=", load_recursive(e))
|
57
|
+
end
|
58
|
+
else
|
59
|
+
ts.send("#{property}=", load_recursive(e))
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
unless links.empty?
|
64
|
+
ts.send("links=", links.map{|l| build_struct(l)})
|
65
|
+
end
|
66
|
+
|
67
|
+
ts
|
68
|
+
end
|
69
|
+
|
70
|
+
# Most of the time Twitter specifies nodes that contain an array of
|
71
|
+
# sub-nodes with a type="array" attribute. There are some nodes that
|
72
|
+
# they dont' do that for, though, including the <ids> node returned
|
73
|
+
# by the social graph methods. This method tries to work in both situations.
|
74
|
+
def array_node?(node)
|
75
|
+
node.attributes['type'] == 'collection'
|
76
|
+
end
|
77
|
+
|
78
|
+
# Nodes which content must not be processed
|
79
|
+
def cdata_node?(node)
|
80
|
+
['xhtml', 'html'].include?(node.attributes['type']) && !node.cdatas.empty?
|
81
|
+
end
|
82
|
+
|
83
|
+
def raw_node?(node)
|
84
|
+
node.name == 'content' && node.attributes['type'] == 'text'
|
85
|
+
end
|
86
|
+
|
87
|
+
# Nodes corresponding to an element repeated with different types
|
88
|
+
def multiple_type?(node)
|
89
|
+
node.name == 'content' && !node.attributes['type'].nil?
|
90
|
+
end
|
91
|
+
|
92
|
+
def fixnum?(value)
|
93
|
+
value =~ /^\d+$/
|
94
|
+
end
|
95
|
+
|
96
|
+
def node_namespace(node)
|
97
|
+
node.namespace.blank? ? nil : node.namespaces.invert[node.namespace]
|
98
|
+
end
|
99
|
+
|
100
|
+
def ignore_attributes?(node)
|
101
|
+
element_name = node.name
|
102
|
+
ns = node_namespace(node)
|
103
|
+
element_name.insert(0, "#{ns}:") if ns
|
104
|
+
|
105
|
+
IGNORE_ATTRIBUTES_FOR.include?(element_name)
|
106
|
+
end
|
107
|
+
|
108
|
+
IGNORE_ATTRIBUTES_FOR = ['ptItem:synopsis', 'ptItem:title']
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|