yanapi 0.1.1 → 0.3.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.
@@ -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