yanapi 0.1.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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 is a Yahoo! Answers API written in Ruby.
1
+ = YANAPI
2
2
 
3
- Example:
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
- require 'rubygems'
6
- require 'yanapi'
7
+ == DESCRIPTION
7
8
 
8
- params = {:appid => 'YahooDemo', :query => 'car'}
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
- query = YANAPI::TermQuery.new(params)
12
+ YANAPI provides a flexible interface to the {Yahoo! Answers}[http://answers.yahoo.com/] search services.
11
13
 
12
- query.get
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.
@@ -1,8 +1,2 @@
1
- require 'yanapi/common'
2
- require 'yanapi/query'
3
- require 'yanapi/term_query'
4
- require 'yanapi/question_query'
5
- require 'yanapi/user_query'
6
- require 'yanapi/category_query'
7
- require 'yanapi/version'
1
+ require 'yanapi/api'
8
2
 
@@ -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)
@@ -34,4 +34,7 @@ module YANAPI
34
34
  # we are out of the response range (but we are under the :start limitations).
35
35
  class EmptyResponse < Error
36
36
  end
37
+
38
+ class UserError < Error
39
+ end
37
40
  end # YANAPI
@@ -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
- attr_accessor :output, :url
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
- # xml|json|php|rss
20
- @output = params[:output] || 'xml'
21
-
22
- # it is a bad idea to alter the parameters hash
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
- result = Nokogiri::XML::Document.parse(http_response.body, nil, nil, 0)
38
- prove_xml(result)
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.to_s
57
- when Net::HTTPMovedPermanently,
58
- Net::HTTPFound,
59
- Net::HTTPSeeOther,
60
- Net::HTTPTemporaryRedirect
61
- raise ContentError.new("#{http_response}:\n\n#{result}")
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 ContentError.new("#{http_response}:\n\n#{result}")
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
- # Returns an URI::HTTP object containing the complete url for the request.
101
+ # It returns an URI::HTTP object containing the complete url for the request.
78
102
  def build_url(params)
79
- search_method = params[:method]
80
- params.delete(:method)
81
-
82
- base_url = [HOST, SERVICE, SERVICE_VERSION, search_method].join('/')
83
-
84
- params = expand_params(params)
85
- reserved_chars = Regexp.new(/[!*'();:@&=+$,\/?#\]\[]/)
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((URI.escape(k.to_s)), unsafe = reserved_chars)
91
- v = URI.escape((URI.escape(v.to_s)), usafe = reserved_chars)
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
- if $DEBUG
97
- $stderr.print 'Joined query parameters: '
98
- $stderr.puts query
99
- end
117
+ base_url = [HOST, SERVICE, SERVICE_VERSION, @method].join('/')
118
+ url = base_url + '?' + query
100
119
 
101
- url = URI.parse(base_url + '?' + query)
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
- allowed_output = [nil, 'xml', 'json', 'rss', 'php']
128
- unless allowed_output.include?(params[:output])
129
- raise Error, "The output type #{params[:output]} is not supported!"
130
- end
131
-
132
- if params[:output] != 'json' && params[:callback]
133
- raise Error, "You may not use callback with output type: #{@output}!"
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
- def prove_php(php)
163
- raise NotImplementedError, 'We do not handle PHP yet!'
164
- end
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