forked from mikedemers/rbing
/
rbing.rb
208 lines (190 loc) · 6.06 KB
/
rbing.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
require "httparty"
require "yaml"
#
# Usage:
#
# bing = RBing.new("YOURAPPID")
#
# rsp = bing.web("ruby")
# puts rsp.results[0].title
# => "Ruby (programming language) - Wikipedia, the free encyclopedia"
#
# rsp = bing.web("ruby", :site => "github.com")
# puts rsp.results[0].url
# => "http://github.com/vim-ruby/vim-ruby/tree/master"
#
# rsp = bing.web("ruby", :site => ["github.com", "rubyforge.org"])
# puts rsp.results[0].url
# => "http://rubyforge.org/"
#
# rsp = bing.news("search engines")
# puts rsp.results[0].title
# => "Microsoft Bing more popular than Yahoo"
#
# rsp = bing.spell("coincidance")
# puts rsp.results[0].value
# => "coincidence"
#
# rsp = bing.instant_answer("How many rods in a furlong?")
# puts rsp.results[0].instant_answer_specific_data.encarta.value
# => "1 furlong = 40 rods"
#
class RBing
# Raised when the API endpoint doesn't return expected response
class APIError < IOError; end
# Convenience wrapper for the response Hash.
# Converts keys to Strings. Crawls through all
# member data and converts any other Hashes it
# finds. Provides access to values through
# method calls, which will convert underscored
# to camel case.
#
# Usage:
#
# rd = ResponseData.new("AlphaBeta" => 1, "Results" => {"Gamma" => 2, "delta" => [3, 4]})
# puts rd.alpha_beta
# => 1
# puts rd.alpha_beta.results.gamma
# => 2
# puts rd.alpha_beta.results.delta
# => [3, 4]
#
class ResponseData < Hash
private
def initialize(data={})
data.each_pair {|k,v| self[k.to_s] = deep_parse(v) }
end
def deep_parse(data)
case data
when Hash
self.class.new(data)
when Array
data.map {|v| deep_parse(v) }
else
data
end
end
def method_missing(*args)
name = args[0].to_s
return self[name] if has_key? name
camelname = name.split('_').map {|w| "#{w[0,1].upcase}#{w[1..-1]}" }.join("")
if has_key? camelname
self[camelname]
else
super *args
end
end
end
include HTTParty
attr_accessor :instance_options
base_uri "https://api.datamarket.azure.com/Data.ashx/Bing/SearchWeb/v1/Web"
format :json
BASE_OPTIONS = [:version, :market, :adult, :query, :appid]
# Query Keywords: <http://help.live.com/help.aspx?project=wl_searchv1&market=en-US&querytype=keyword&query=redliub&tmt=&domain=www.bing.com:80>
#
QUERY_KEYWORDS = [:site, :language, :contains, :filetype, :inanchor, :inbody, :intitle, :ip, :loc, :location, :prefer, :feed, :hasfeed, :url]
RESERVED_OPTIONS = [:top, :skip]
# Source Types: <http://msdn.microsoft.com/en-us/library/dd250847.aspx>
#
SOURCES = %w(Web)
# Set up methods for each search source:
# +ad+, +image+, +instant_answer+, +news+, +phonebook+, +related_search+,
# +spell+ and +web+
#
# Example:
#
# bing = RBing.new(YOUR_APP_ID)
# bing.web("ruby gems", :count => 10)
#
SOURCES.each do |source|
fn = source.to_s.gsub(/[a-z][A-Z]/) {|c| "#{c[0,1]}_#{c[1,1]}" }.downcase
class_eval "def #{fn}(query, options={}) ; search('#{source}', query, options) ; end"
end
# issues a search for +query+ in +source+
#
def search(source, query, options={})
rsp = self.class.get("", options_for(source, query, options))
if rsp.response.is_a?(Net::HTTPOK)
ResponseData.new(rsp['d']) if rsp
else
raise RBing::APIError.new(
rsp.request.to_yaml + "\n" +
rsp.response.to_yaml
)
end
end
private
# instantiates a new RBing client with the given +app_id+.
# +options+ can contain values to be passed with each query.
#
def initialize(app_id=nil, options={})
@instance_options = options.merge(:AppId => (app_id || user_app_id))
end
# constructs a query string for the given
# +query+ and the optional query +options+
#
def build_query(query, options={})
queries = {}
queries[:Query] = "'#{query}'"
QUERY_KEYWORDS.each do |kw|
next unless options[kw]
if options[kw].is_a? Array
kw_query = options[kw].map {|s| "#{kw}:#{s}".strip }.join(" OR ")
queries[kw.to_sym] = "(#{kw_query})"
else
queries[kw.to_sym] = options[kw]
end
end
queries
end
# returns +options+ with its keys converted to
# strings and any keys in +exclude+ omitted.
#
def filter_hash(options, exclude=[])
ex = exclude.inject({}) {|h,k| h[k.to_s] = true; h }
options.inject({}) {|h,kv| h[kv[0]] = kv[1] unless ex[kv[0].to_s]; h }
end
# returns an options Hash suitable for passing to
# HTTParty's +get+ method
#
def options_for(type, query, options={})
opts = instance_options.merge(filter_hash(options, BASE_OPTIONS))
opts.merge!(build_query(query, options))
source_options = filter_hash(options, [:http] + BASE_OPTIONS + QUERY_KEYWORDS)
opts.merge!(scope_source_options(type, source_options))
RESERVED_OPTIONS.each do |reserved_option|
next unless options[reserved_option]
opts.merge!("$#{reserved_option}" => options[reserved_option])
opts.delete(reserved_option)
opts.delete('Web.' + "#{reserved_option}") # Why is this needed? What is causing it to set this?
end
opts.merge!('$format' => 'JSON')
authentication_options = {:basic_auth => {
:username => '',
:password => "#{@instance_options[:AppId]}"
}}
opts.delete(:AppId)
http_options = options[:http] || {}
http_options.merge!(authentication_options)
http_options.merge(:query => opts)
end
# returns a Hash containing the data in +options+
# with the keys prefixed with +type+ and '.'
#
def scope_source_options(type, options={})
options.inject({}) {|h,kv| h["#{type}.#{kv[0]}"] = kv[1]; h }
end
# returns the user's default app id, if one has been
# defined in ~/.rbing_app_id
#
def user_app_id(force=false)
@user_app_id = nil if force
@user_app_id ||= read_user_app_id
end
# reads the App Id stored in ~/.rbing_app_id
#
def read_user_app_id
fn = File.join(RUBY_PLATFORM =~ /mswin32/ ? ENV['USERPROFILE'] : ENV['HOME'], ".rbing_app_id")
File.read(fn).strip if File.exists?(fn)
end
end