yanapi 0.1.1 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- data/CHANGELOG +22 -0
- data/README +89 -7
- data/lib/yanapi.rb +1 -7
- data/lib/yanapi/api.rb +97 -0
- data/lib/yanapi/category_query.rb +14 -1
- data/lib/yanapi/error.rb +3 -0
- data/lib/yanapi/query.rb +82 -109
- data/lib/yanapi/question_query.rb +8 -2
- data/lib/yanapi/term_query.rb +16 -1
- data/lib/yanapi/user_query.rb +16 -3
- data/lib/yanapi/version.rb +1 -1
- data/test/data/bad_xml.txt +0 -221
- data/test/data/code_400.txt +15 -0
- data/test/data/code_403.txt +15 -0
- data/test/data/code_503.txt +15 -0
- data/test/integration_test_data/test_category_query.txt +61 -0
- data/test/integration_test_data/test_empty_response.txt +13 -0
- data/test/integration_test_data/test_question_query.txt +49 -0
- data/test/integration_test_data/test_term_query.txt +62 -0
- data/test/integration_test_data/test_user_query.txt +62 -0
- data/test/test_api.rb +154 -0
- data/test/test_category_query.rb +12 -1
- data/test/test_common.rb +1 -1
- data/test/test_helper.rb +31 -0
- data/test/test_integration.rb +129 -0
- data/test/test_query.rb +131 -66
- data/test/test_question_query.rb +14 -1
- data/test/test_term_query.rb +20 -10
- data/test/test_user_query.rb +15 -1
- data/test/test_version.rb +12 -0
- data/test/test_yanapi.rb +3 -2
- metadata +60 -14
- data/README.rdoc +0 -23
- data/Rakefile +0 -14
data/CHANGELOG
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
=== COMPLETED
|
2
|
+
==== 0.3.1
|
3
|
+
All four search methods implemented.
|
4
|
+
==== 0.1.1
|
5
|
+
Small fixes in the documentation.
|
6
|
+
==== 0.1.0
|
7
|
+
The search methods <tt>questionSearch</tt> and <tt>getByCategory</tt>.
|
8
|
+
==== 0.0.1
|
9
|
+
Initial release of the lib.
|
10
|
+
|
11
|
+
|
12
|
+
=== PLANNED
|
13
|
+
==== 0.4.0
|
14
|
+
Enhance the documentation.
|
15
|
+
|
16
|
+
Implement multivalue parameters.
|
17
|
+
==== 0.5.0
|
18
|
+
==== 0.6.0
|
19
|
+
==== 0.7.0
|
20
|
+
==== 0.8.0
|
21
|
+
==== 0.9.0
|
22
|
+
==== 1.0.0
|
data/README
CHANGED
@@ -1,12 +1,94 @@
|
|
1
|
-
YANAPI
|
1
|
+
= YANAPI
|
2
2
|
|
3
|
-
|
3
|
+
* {RubyGems}[http://rubygems.org/gems/yanapi]
|
4
|
+
* Developers {Homepage}[http://www.uni-trier.de/index.php?id=24140]
|
5
|
+
* {YANAPI Project Page}[http://yanapi.rubyforge.org/]
|
4
6
|
|
5
|
-
|
6
|
-
require 'yanapi'
|
7
|
+
== DESCRIPTION
|
7
8
|
|
8
|
-
|
9
|
+
YANAPI is an API for Yahoo! Answers web services. It has been developed
|
10
|
+
in {Ruby}[http://www.ruby-lang.org].
|
9
11
|
|
10
|
-
|
12
|
+
YANAPI provides a flexible interface to the {Yahoo! Answers}[http://answers.yahoo.com/] search services.
|
11
13
|
|
12
|
-
|
14
|
+
It supports four search types:
|
15
|
+
* key word search;
|
16
|
+
* search based on the category name or the category ID;
|
17
|
+
* search for a precise question ID;
|
18
|
+
* search for a questions posted by a user with an ID.
|
19
|
+
|
20
|
+
It is possible to restrict key word based search through a category.
|
21
|
+
|
22
|
+
Question Search and User Search cannot be extended by key words or category IDs.
|
23
|
+
|
24
|
+
Yanapi tries to as flexible as possible. It restricts unallowed parameter
|
25
|
+
combinations and forces using mandatory ones. But it doesn't care about defaults.
|
26
|
+
For example, as for this writing the default output value is an xml based format.
|
27
|
+
If it changes in the future, the user will be responsible to choose the format.
|
28
|
+
No defaults are hardcoded in YANAPI.
|
29
|
+
YANAPI provides the minimal acceptable query.
|
30
|
+
|
31
|
+
== SYNOPSIS
|
32
|
+
|
33
|
+
YANAPI requires a parameter hash with two dimensions:
|
34
|
+
* the key +:method+ points to one of possible search methods
|
35
|
+
(<tt>'questionSearch', 'getByCategory', 'getByUser', 'getQuestion'</tt>);
|
36
|
+
* the key +:query_params+ points to a parameter hash, the names of the parameters
|
37
|
+
and their values correspond to the semantics given
|
38
|
+
on {the official page}[http://developer.yahoo.com/answers/];
|
39
|
+
* values in +:query_params+ may be +String+, +Fixnum+ or +Array+, the latter
|
40
|
+
provides the opportunity for multiple values which is possible and sometimes
|
41
|
+
reasonable for such keys as +:region+, +:category_id+ or +:category_name+.
|
42
|
+
|
43
|
+
A small example shall demostrate the usage:
|
44
|
+
require 'yanapi'
|
45
|
+
params = {
|
46
|
+
:method => 'questionSearch',
|
47
|
+
:query_params => {
|
48
|
+
:appid => 'YahooDemo',
|
49
|
+
:query => 'Haus',
|
50
|
+
:search_in => 'question',
|
51
|
+
:type => 'resolved',
|
52
|
+
:results => 2,
|
53
|
+
:output => 'xml'
|
54
|
+
}
|
55
|
+
}
|
56
|
+
api = YANAPI::API.new(params)
|
57
|
+
api.get # => default xml structure
|
58
|
+
|
59
|
+
|
60
|
+
For details on particular keys and defaults see {the official description}[http://developer.yahoo.com/answers/] and the RDoc documentation in this libruary.
|
61
|
+
|
62
|
+
== EXCEPTION HIERARCHY
|
63
|
+
While using YANAPI you can face three kinds of errors:
|
64
|
+
* <tt>YANAPI::UserError</tt>;
|
65
|
+
* <tt>YANAPI::ExternalError</tt>;
|
66
|
+
* <tt>YANAPI::ContentError</tt>.
|
67
|
+
|
68
|
+
See the RDoc documentation on semantics of these errors.
|
69
|
+
|
70
|
+
All of them are subcalsses of <tt>YANAPI::Error</tt> which is in turn a subclass
|
71
|
+
of the standard +RuntimeError+.
|
72
|
+
|
73
|
+
If you want to intercept any and every exception thrown by YANAPI simply rescue
|
74
|
+
<tt>YANAPI::Error</tt>.
|
75
|
+
|
76
|
+
== SEARCH STRATEGIES
|
77
|
+
This section will describe possible applications of YANAPI.
|
78
|
+
|
79
|
+
== CHANGELOG
|
80
|
+
See CHANGELOG.
|
81
|
+
|
82
|
+
== CAUTION
|
83
|
+
This library is <b>work in process</b>! Though the interface is mostly complete,
|
84
|
+
you might face some not implemented features.
|
85
|
+
|
86
|
+
Please contact me with your suggestions, bug reports and feature requests.
|
87
|
+
|
88
|
+
|
89
|
+
|
90
|
+
== LICENSE
|
91
|
+
|
92
|
+
YANAPI is a copyrighted software by Andrei Beliankou, 2011.
|
93
|
+
You may use, redistribute and change it under the terms
|
94
|
+
provided in the LICENSE file.
|
data/lib/yanapi.rb
CHANGED
data/lib/yanapi/api.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# :title: YANAPI, Yahoo! Answers API
|
2
|
+
# :main: README.rdoc
|
3
|
+
|
4
|
+
require 'yanapi/version'
|
5
|
+
require 'yanapi/term_query'
|
6
|
+
require 'yanapi/category_query'
|
7
|
+
require 'yanapi/user_query'
|
8
|
+
require 'yanapi/question_query'
|
9
|
+
|
10
|
+
module YANAPI
|
11
|
+
class API
|
12
|
+
REQUIRED_PARAMS = [:method, :query_params]
|
13
|
+
ACCEPTED_METHODS = ['questionSearch',
|
14
|
+
'getByUser',
|
15
|
+
'getByCategory',
|
16
|
+
'getQuestion'
|
17
|
+
]
|
18
|
+
|
19
|
+
def initialize(params)
|
20
|
+
|
21
|
+
@params = check_params(params)
|
22
|
+
|
23
|
+
@query = case @params[:method]
|
24
|
+
when 'questionSearch'
|
25
|
+
TermQuery.new(@params)
|
26
|
+
when 'getByCategory'
|
27
|
+
CategoryQuery.new(@params)
|
28
|
+
when 'getByUser'
|
29
|
+
UserQuery.new(@params)
|
30
|
+
when 'getQuestion'
|
31
|
+
QuestionQuery.new(@params)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# This is a connector to the specific method in the query.
|
36
|
+
def get
|
37
|
+
@query.get
|
38
|
+
end
|
39
|
+
|
40
|
+
def version
|
41
|
+
VERSION
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
def check_params(params)
|
46
|
+
# It should be an instance on non empty hash.
|
47
|
+
unless params.instance_of?(Hash)
|
48
|
+
fail(UserError, "Params should be a Hash, not #{params.class}!")
|
49
|
+
end
|
50
|
+
|
51
|
+
if params.empty?
|
52
|
+
fail(UserError, 'The params hash is empty!')
|
53
|
+
end
|
54
|
+
|
55
|
+
# It should contain required keys and be two dimensional.
|
56
|
+
missed_keys = []
|
57
|
+
REQUIRED_PARAMS.each do |key|
|
58
|
+
unless params.has_key?(key)
|
59
|
+
missed_keys << key
|
60
|
+
end
|
61
|
+
end
|
62
|
+
unless missed_keys.empty?
|
63
|
+
fail(UserError,
|
64
|
+
"You should provide: <:#{missed_keys.join('>, <:')}>!")
|
65
|
+
end
|
66
|
+
|
67
|
+
unless params[:query_params].instance_of?(Hash)
|
68
|
+
key = params[:query_params]
|
69
|
+
fail(UserError,
|
70
|
+
"<:query_params> should be a Hash, not #{key.class}!")
|
71
|
+
end
|
72
|
+
|
73
|
+
if params[:query_params].empty?
|
74
|
+
fail(UserError, '<:query_params> is empty!')
|
75
|
+
end
|
76
|
+
|
77
|
+
# It should accept only allowed methods.
|
78
|
+
unless ACCEPTED_METHODS.include?(params[:method])
|
79
|
+
fail(UserError,
|
80
|
+
"<#{params[:method]}> is not a valid search method!")
|
81
|
+
end
|
82
|
+
|
83
|
+
# It should warn about superfluous keys.
|
84
|
+
superfluous_keys = []
|
85
|
+
params.each_key do |key|
|
86
|
+
unless REQUIRED_PARAMS.include?(key)
|
87
|
+
superfluous_keys << key
|
88
|
+
end
|
89
|
+
end
|
90
|
+
unless superfluous_keys.empty?
|
91
|
+
warn "Keys ignored: <:#{superfluous_keys.join('>, <:')}>!"
|
92
|
+
end
|
93
|
+
|
94
|
+
params
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -1,8 +1,21 @@
|
|
1
|
+
require 'yanapi/query'
|
2
|
+
require 'yanapi/common'
|
1
3
|
|
2
4
|
module YANAPI
|
3
5
|
|
4
6
|
class CategoryQuery < Query
|
5
|
-
|
7
|
+
VALID_PARAMS = [:category_id,
|
8
|
+
:category_name,
|
9
|
+
:date_range,
|
10
|
+
:region,
|
11
|
+
:results,
|
12
|
+
:sort,
|
13
|
+
:start,
|
14
|
+
:type
|
15
|
+
]
|
16
|
+
# Semantics differ here: OR, not AND.
|
17
|
+
REQUIRED_PARAMS = [[:category_id, :category_name]]
|
18
|
+
|
6
19
|
include Common
|
7
20
|
|
8
21
|
def initialize(params)
|
data/lib/yanapi/error.rb
CHANGED
data/lib/yanapi/query.rb
CHANGED
@@ -1,5 +1,4 @@
|
|
1
1
|
# This code is based on ideas from "rc_rest" and "yahoo" gems.
|
2
|
-
# gem install nokogiri
|
3
2
|
require 'nokogiri'
|
4
3
|
require 'net/http'
|
5
4
|
require 'uri'
|
@@ -12,19 +11,18 @@ module YANAPI
|
|
12
11
|
HOST = 'http://answers.yahooapis.com'
|
13
12
|
SERVICE = 'AnswersService'
|
14
13
|
SERVICE_VERSION = 'V1'
|
15
|
-
|
16
|
-
|
14
|
+
VALID_PARAMS = [:appid, :output, :callback]
|
15
|
+
REQUIRED_PARAMS = [:appid]
|
16
|
+
VALID_OUTPUT_FORMATS = [nil, 'xml', 'php', 'rss', 'json']
|
17
17
|
|
18
|
+
# It accepts a two dimensional hash:
|
19
|
+
# {:method => 'questionSearch', :query_params =>
|
20
|
+
# {:appid => 'YahooDemo', :query => 'Haus'}}
|
18
21
|
def initialize(params)
|
19
|
-
|
20
|
-
@
|
21
|
-
|
22
|
-
|
23
|
-
# that's why we duplicate the parameters
|
24
|
-
check_params(params)
|
25
|
-
@url = build_url(params.dup)
|
26
|
-
|
27
|
-
$stderr.puts 'Full url:', @url if $DEBUG
|
22
|
+
@method = params[:method]
|
23
|
+
@params = check_params(params[:query_params])
|
24
|
+
@url = build_url(@params)
|
25
|
+
@output = @params[:output] || 'xml'
|
28
26
|
end
|
29
27
|
|
30
28
|
# main method
|
@@ -34,33 +32,27 @@ module YANAPI
|
|
34
32
|
case @output
|
35
33
|
when 'xml'
|
36
34
|
# Set the value to STRICT (0), otherwise no errors will be raised!
|
37
|
-
|
38
|
-
prove_xml(
|
35
|
+
xml = Nokogiri::XML::Document.parse(http_response.body, nil, nil, 0)
|
36
|
+
result = prove_xml(xml) ? http_response.body : nil
|
39
37
|
when 'json'
|
40
38
|
raise NotImplementedError, 'We do not handle JSON yet!'
|
41
|
-
result = ''
|
42
|
-
prove_json(result)
|
43
39
|
when 'php'
|
44
40
|
raise NotImplementedError, 'We do not handle PHP yet!'
|
45
|
-
result = ''
|
46
|
-
prove_php(result)
|
47
41
|
when 'rss'
|
48
42
|
raise NotImplementedError, 'We do not handle RSS yet!'
|
49
|
-
result = ''
|
50
|
-
prove_rss(result)
|
51
43
|
end
|
52
44
|
|
53
45
|
# TODO: make a fine grained distinction
|
54
46
|
case http_response
|
55
47
|
when Net::HTTPSuccess
|
56
|
-
return result
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
raise
|
48
|
+
return result
|
49
|
+
|
50
|
+
when Net::HTTPBadRequest,
|
51
|
+
Net::HTTPForbidden,
|
52
|
+
Net::HTTPServiceUnavailable
|
53
|
+
raise ExternalError.new("#{http_response}:\n\n#{result}")
|
62
54
|
else
|
63
|
-
raise
|
55
|
+
raise ExternalError.new("#{http_response}:\n\n#{result}")
|
64
56
|
end
|
65
57
|
|
66
58
|
# are all external errors caught here?
|
@@ -71,67 +63,76 @@ module YANAPI
|
|
71
63
|
rescue Nokogiri::XML::SyntaxError => e
|
72
64
|
raise ContentError.new(e)
|
73
65
|
end # get
|
74
|
-
|
66
|
+
|
67
|
+
#########
|
75
68
|
protected
|
69
|
+
# It checks params and returns a validated params hash.
|
70
|
+
def check_params(params)
|
71
|
+
# It requires <:appid> to be present.
|
72
|
+
unless params.has_key?(:appid)
|
73
|
+
fail UserError, 'APPID is missing!'
|
74
|
+
end
|
75
|
+
|
76
|
+
# It accepts only valid output formats.
|
77
|
+
unless VALID_OUTPUT_FORMATS.include?(params[:output])
|
78
|
+
fail UserError, "The output type <#{params[:output]}> is not supported!"
|
79
|
+
end
|
80
|
+
|
81
|
+
# It rejects <:callback> for all output formats but <json>.
|
82
|
+
if params[:output] != 'json' && params[:callback]
|
83
|
+
fail UserError,
|
84
|
+
"Output type #{params[:output]} is incompatible with <:callback>!"
|
85
|
+
end
|
86
|
+
|
87
|
+
# It accepts only unique values for every parameter,
|
88
|
+
# they can be Strings or Numbers.
|
89
|
+
# We do not support multiple values for categories and regions yet.
|
90
|
+
# Multiple values will be provided as Arrays:
|
91
|
+
# :category_id => [123, 456, 789].
|
92
|
+
params.each_pair do |k, v|
|
93
|
+
unless (v.instance_of?(String) || v.instance_of?(Fixnum))
|
94
|
+
fail UserError, "The value <:#{k}> is not unique!"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
params
|
99
|
+
end # check_params
|
76
100
|
|
77
|
-
#
|
101
|
+
# It returns an URI::HTTP object containing the complete url for the request.
|
78
102
|
def build_url(params)
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
# Every string is escaped twice.
|
87
|
-
# First time to allow only ascii characters.
|
88
|
-
# Second time to escape all url reserved chars.
|
103
|
+
# URI does not know if our string contains special characters or not.
|
104
|
+
# That's why it treats them as special, we need a stricter form and
|
105
|
+
# provide a list of characters which should be escaped.
|
106
|
+
reserved_chars = Regexp.new(/[ !*'();:@&=+$,\/?#\]\[]/)
|
107
|
+
|
108
|
+
|
109
|
+
expanded_params = expand_params(params) # It is an array now!
|
89
110
|
escaped_params = params.collect do |k, v|
|
90
|
-
k = URI.escape(
|
91
|
-
v = URI.escape(
|
111
|
+
k = URI.escape(k.to_s, unsafe = reserved_chars)
|
112
|
+
v = URI.escape(v.to_s, unsafe = reserved_chars)
|
92
113
|
"#{k}=#{v}"
|
93
114
|
end
|
94
115
|
|
95
116
|
query = escaped_params.join('&')
|
96
|
-
|
97
|
-
|
98
|
-
$stderr.puts query
|
99
|
-
end
|
117
|
+
base_url = [HOST, SERVICE, SERVICE_VERSION, @method].join('/')
|
118
|
+
url = base_url + '?' + query
|
100
119
|
|
101
|
-
|
102
|
-
|
103
|
-
return url
|
120
|
+
URI.parse(url)
|
104
121
|
end # build_url
|
105
|
-
|
106
|
-
# Check params, my sheet
|
107
|
-
#--
|
108
|
-
# validates presense of :method
|
109
|
-
# validates type of :method
|
110
|
-
# validates presense of :appid
|
111
|
-
# validates uniqueness of all params
|
112
|
-
# (category_name|category_id may or region be repeated)
|
113
|
-
# default values will be propagated, not deleted (defaults can change)
|
114
|
-
def check_params(params)
|
115
|
-
unless params.include?(:appid)
|
116
|
-
raise Error, 'APPID is missing!'
|
117
|
-
end
|
118
|
-
unless params.include?(:method)
|
119
|
-
raise Error, 'Search Method is missing!'
|
120
|
-
end
|
121
|
-
|
122
|
-
allowed_methods = %w(questionSearch getByUser getByCategory getQuestion)
|
123
|
-
unless allowed_methods.include?(params[:method])
|
124
|
-
raise Error, "The search method #{params[:method]} is not supported!"
|
125
|
-
end
|
126
122
|
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
123
|
+
# It expands multiple values to key-val pairs and returns a params array.
|
124
|
+
def expand_params(params)
|
125
|
+
expanded_params = []
|
126
|
+
|
127
|
+
params.each_pair do |k, v|
|
128
|
+
if v.instance_of?(Array)
|
129
|
+
v.each { |val| expanded_params << [k, val] }
|
130
|
+
else
|
131
|
+
expanded_params << [k, v]
|
132
|
+
end
|
134
133
|
end
|
134
|
+
|
135
|
+
expanded_params.sort_by { |k, v| [k.to_s, v.to_s] }
|
135
136
|
end
|
136
137
|
|
137
138
|
# proves the presense of an error
|
@@ -147,41 +148,13 @@ module YANAPI
|
|
147
148
|
end
|
148
149
|
|
149
150
|
question = xml.at_xpath('//xmlns:Question', xml.root.namespaces)
|
150
|
-
unless question
|
151
|
-
message = 'The server returned an empty Result Set.'
|
152
|
-
raise EmptyResponse.new(message)
|
153
|
-
end
|
154
|
-
end
|
155
|
-
|
156
|
-
#
|
157
|
-
def prove_json(json)
|
158
|
-
raise NotImplementedError, 'We do not handle JSON yet!'
|
159
|
-
end
|
160
151
|
|
161
|
-
|
162
|
-
|
163
|
-
|
164
|
-
|
165
|
-
|
166
|
-
#
|
167
|
-
def prove_rss(rss)
|
168
|
-
raise NotImplementedError, 'We do not handle RSS yet!'
|
169
|
-
end
|
170
|
-
|
171
|
-
def expand_params(params)
|
172
|
-
expanded_params = []
|
173
|
-
|
174
|
-
params.each do |k,v|
|
175
|
-
if v.respond_to? :each and not String === v then
|
176
|
-
v.each { |s| expanded_params << [k, s] }
|
177
|
-
else
|
178
|
-
expanded_params << [k, v]
|
179
|
-
end
|
152
|
+
if question
|
153
|
+
xml
|
154
|
+
else
|
155
|
+
nil
|
180
156
|
end
|
181
|
-
|
182
|
-
expanded_params.sort_by { |k,v| [k.to_s, v.to_s] }
|
183
|
-
end
|
184
|
-
|
157
|
+
end
|
185
158
|
end # Query
|
186
159
|
|
187
160
|
end # YANAPI
|