bing-search 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3f89243c5039d162db74eea16a768640ca39c090
4
+ data.tar.gz: 5bbed193646f93ccd1b1bfa802cea73612a426ee
5
+ SHA512:
6
+ metadata.gz: 7502295d14673dbe7880875c4db6b52edc09c97d9e39a2ff50a178ab18832bc784e1ba0af297b4d5a4c86aeea44bb34ecc99c5bb10e4e151a4f2a2c263bcbe85
7
+ data.tar.gz: 1639f625fdedaf8efceb98224e6da870bc10d5d92b387aed79fd90d30a6cb2eece3e033f1d1bb8062ba2b3b071f8e1adb8443bdd3595c26f7e76dc107781d607
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2015 Jonah Burke
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,165 @@
1
+ # bing-search
2
+
3
+ A Ruby client for the [Bing Search API](http://datamarket.azure.com/dataset/bing/search).
4
+
5
+ ## Getting Started
6
+
7
+ ### Installation
8
+
9
+ ```bash
10
+ gem install bing-search
11
+ ```
12
+
13
+ ### Signup
14
+
15
+ Sign up for the [Bing Search API](https://datamarket.azure.com/dataset/bing/search) or the [Web-Only Bing Search API](https://datamarket.azure.com/dataset/bing/searchweb) at the Microsoft Azure Marketplace. Then retrieve your Account Key from the [My Account](https://datamarket.azure.com/account) section of the marketplace and provide it as shown below.
16
+
17
+ ## Basics
18
+
19
+ To use bing-search, first supply your Account Key:
20
+
21
+ ```ruby
22
+ BingSearch.account_key = 'hzy9+Y6...'
23
+ ```
24
+
25
+ Then, use {BingSearch.web} to search for web pages:
26
+
27
+ ```ruby
28
+ results = BingSearch.web('Dirac')
29
+ ```
30
+
31
+ Or, use the other {BingSearch} class methods to search for images, news, video, related searches, and spelling suggestions:
32
+
33
+ ```ruby
34
+ BingSearch.spelling('Feinman').first.suggestion # => "Feynman"
35
+ ```
36
+
37
+ The type of result depends on the kind of search:
38
+
39
+ ```ruby
40
+ BingSearch.web('Gell-Mann').class # => WebResult
41
+ BingSearch.image('Pauli').class # => ImageResult
42
+ BingSearch.video('von Neumann').class # => VideoResult
43
+ ```
44
+
45
+ And each result type has its own attributes:
46
+
47
+ ```ruby
48
+ web = BingSearch.web('Gell-Mann').first
49
+ web.summary # => "Murray Gell-Mann (born September 15, 1929) is an American physicist ..."
50
+
51
+ image = BingSearch.image('Pauli').first
52
+ image.media_type # => "image/jpeg"
53
+
54
+ video = BingSearch.video('von Neumann').first
55
+ video.duration # => 151000
56
+ ```
57
+
58
+ See the documentation of the result types for a full list of the attributes.
59
+
60
+ ## Options
61
+
62
+ The search methods take options that control the number of results returned;
63
+
64
+ ```ruby
65
+ BingSearch.web('Dyson', limit: 5).count # => 5
66
+ ```
67
+
68
+ the size, orientation, and contents of images;
69
+
70
+ ```ruby
71
+ BingSearch.image 'Tesla', filters: [:large, :wide, :photo, :face]
72
+ ```
73
+
74
+ whether to {BingSearch::HIGHLIGHT_DELIMITER highlight} query terms in the results;
75
+
76
+ ```ruby
77
+ BingSearch.news('Hawking', highlighting: true).first.title # => "How Intel Gave Stephen Hawking a Voice"
78
+ ```
79
+
80
+ and many other aspects of the search. Note that "enumeration" options—those whose values are module-level constants—may be provided as underscored symbols:
81
+
82
+ ```ruby
83
+ # equivalent searches
84
+ BingSearch.news 'Higgs', category: BingSearch::NewsCategory::ScienceAndTechnology
85
+ BingSearch.news 'Higgs', category: :science_and_technology
86
+ ```
87
+
88
+ See {BingSearch::Client} for exhaustive documentation of the options.
89
+
90
+ ## Composite Searches
91
+
92
+ To retrieve multiple result types at once, use {BingSearch.composite}:
93
+
94
+ ```ruby
95
+ result = BingSearch.composite('Majorana', [:web, :image, :news])
96
+ ```
97
+
98
+ The result is a {BingSearch::CompositeResult} ...
99
+
100
+ ```ruby
101
+ result.class # => BingSearch::CompositeResult
102
+ ```
103
+
104
+ ... containing an array for each result type:
105
+
106
+ ```ruby
107
+ result.web.first.class # => BingSearch::WebResult
108
+ result.image.first.class # => BingSearch::ImageResult
109
+ result.news.first.class # => BingSearch::NewsResult
110
+ ```
111
+
112
+ All of the single-type search options are supported in composite searches, though the names may have prefixes to specify the type they pertain to:
113
+
114
+ ```ruby
115
+ BingSearch.composite 'Fermi', [:image, :video], image_filters: [:small], video_filters: [:short]
116
+ ```
117
+
118
+ Composite searches also give you access more data about the search including the total number of results in the Bing index and whether Bing corrected apparent errors in the query text:
119
+
120
+ ```ruby
121
+ result = BingSearch.composite('Feyman', [:web, :image, :news])
122
+ result.web_total # => 2400000
123
+ result.altered_query # => "feynman"
124
+ ```
125
+
126
+ ## Web-Only API
127
+
128
+ To use the less expensive [web-only API](https://datamarket.azure.com/dataset/bing/searchweb), set {BingSearch.web_only}:
129
+
130
+ ```ruby
131
+ BingSearch.web_only = true
132
+ BingSearch.news 'Newton' # => BingSearch::ServiceError
133
+ BingSearch.web 'Newton'
134
+ ```
135
+
136
+ ## BingSearch::Client
137
+
138
+ {BingSearch::Client} is the class underlying the {BingSearch} class methods. You can use it on its own to run multiple searches over a single TCP connection:
139
+
140
+ ```ruby
141
+ BingSearch::Client.open do |client|
142
+ client.web 'Lee'
143
+ client.web 'Wu'
144
+ client.web 'Yang'
145
+ end
146
+ ```
147
+
148
+ Or to override global settings:
149
+
150
+ ```ruby
151
+ client = BingSearch::Client.new(access_key: 'hzy9+Y6...', web_only: true)
152
+ ```
153
+
154
+ ## Tests
155
+
156
+ To run the tests:
157
+
158
+ 1. Sign up for both the standard and web-only APIs
159
+ 2. Set the environment variable BING\_SEARCH\_ACCESS\_KEY to your Access Key
160
+ 3. `rake`
161
+
162
+ ## Contributing
163
+
164
+ Please submit issues and pull requests to [jonahb/bing-search](http://github.com/jonahb/bing-search) on GitHub.
165
+
@@ -0,0 +1,93 @@
1
+ %w{
2
+ client
3
+ enums
4
+ errors
5
+ models
6
+ version
7
+ }.each do |file|
8
+ require File.expand_path("../bing-search/#{file}", __FILE__)
9
+ end
10
+
11
+ module BingSearch
12
+
13
+ HIGHLIGHT_DELIMITER = "\u{e001}"
14
+
15
+ class << self
16
+ # An Access Key obtained from the Azure Marketplace. You can set this
17
+ # attribute once instead of instantiating each {Client} with an Access Key.
18
+ # @return [String]
19
+ attr_accessor :access_key
20
+
21
+ # Whether to use the less expensive web-only API
22
+ # @return [Boolean]
23
+ attr_accessor :web_only
24
+
25
+ # Convenience method that creates a {Client} and searches for web pages.
26
+ # Takes the same arguments as {Client#web}. Set {access_key} before calling.
27
+ # @return (see Client#web)
28
+ # @see Client#web
29
+ #
30
+ def web(*args)
31
+ Client.new.web *args
32
+ end
33
+
34
+ # Convenience method that creates a {Client} and searches for images. Takes
35
+ # the same arguments as {Client#image}. Set {access_key} before calling.
36
+ # @return (see Client#image)
37
+ # @see Client#image
38
+ #
39
+ def image(*args)
40
+ Client.new.image *args
41
+ end
42
+
43
+ # Convenience method that creates a {Client} and searches for videos. Takes
44
+ # the same arguments as {Client#video}. Set {access_key} before calling.
45
+ # @return (see Client#video)
46
+ # @see Client#video
47
+ #
48
+ def video(*args)
49
+ Client.new.video *args
50
+ end
51
+
52
+ # Convenience method that creates a {Client} and searches for news. Takes
53
+ # the same arguments as {Client#news}. Set {access_key} before calling.
54
+ # @return (see Client#news)
55
+ # @see Client#news
56
+ #
57
+ def news(*args)
58
+ Client.new.news *args
59
+ end
60
+
61
+ # Convenience method that creates a {Client} and searches for related
62
+ # queries. Takes the same arguments as {Client#related_search}. Set {access_key}
63
+ # before calling.
64
+ # @return (see Client#related_search)
65
+ # @see Client#related_search
66
+ #
67
+ def related_search(*args)
68
+ Client.new.related_search *args
69
+ end
70
+ alias_method :related, :related_search
71
+
72
+ # Convenience method that creates a {Client} and corrects spelling in the
73
+ # query text. Takes the same arguments as {Client#related_search}. Set
74
+ # {access_key} before calling.
75
+ # @return (see Client#spelling_suggestions)
76
+ # @see Client#spelling_suggestions
77
+ #
78
+ def spelling_suggestions(*args)
79
+ Client.new.spelling_suggestions *args
80
+ end
81
+ alias_method :spelling, :spelling_suggestions
82
+
83
+ # Convenience method that creates a {Client} and searches multiple sources
84
+ # Takes the same arguments as {Client#related_search}. Set {access_key} before
85
+ # calling.
86
+ # @return (see Client#composite)
87
+ # @see Client#composite
88
+ #
89
+ def composite(*args)
90
+ Client.new.composite *args
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,614 @@
1
+ require 'date'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'uri'
5
+ require 'active_support/core_ext/hash/slice'
6
+ require 'active_support/core_ext/string/inflections'
7
+ require_relative 'errors'
8
+ require_relative 'models'
9
+ require_relative 'enums'
10
+
11
+ module BingSearch
12
+ class Client
13
+ # The Access Key obtained from the Azure Marketplace
14
+ # @return [String]
15
+ attr_reader :access_key
16
+
17
+ # Whether to use the less expensive web-only API
18
+ # @return [Boolean]
19
+ attr_reader :web_only
20
+
21
+
22
+ # @!group Constructors
23
+
24
+ # @param [String, nil] access_key
25
+ # An Access Key obtained from the Azure Marketplace. If nil,
26
+ # {BingSearch.access_key} is assumed.
27
+ # @param [Boolean, nil] web_only
28
+ # Whether to use the less expensive web-only API. If nil,
29
+ # {BingSearch.web_only} is assumed.
30
+ #
31
+ def initialize(access_key: nil, web_only: nil)
32
+ @access_key = access_key || BingSearch.access_key
33
+ @web_only = web_only.nil? ? BingSearch.web_only : web_only
34
+
35
+ unless @access_key
36
+ raise ArgumentError, "Pass an Access Key or set BingSearch.access_key"
37
+ end
38
+ end
39
+
40
+
41
+ # @!group Sessions
42
+
43
+ # Opens a client and yields it to the given block. Takes the same arguments
44
+ # as {#initialize}.
45
+ # @see #initialize
46
+ # @yieldparam [Client] client
47
+ # @return [Client]
48
+ # @see #open
49
+ #
50
+ def self.open(*args)
51
+ raise "Block required" unless block_given?
52
+ client = new(*args)
53
+ client.open { yield client }
54
+ client
55
+ end
56
+
57
+ # Opens the client, creating a new TCP connection.
58
+ #
59
+ # If a block is given, yields to the block, closes the client when the
60
+ # block returns, and returns the return value of the block. If a
61
+ # block is not given, returns self and leaves the client open, relying on
62
+ # the caller to close the client with {#close}.
63
+ #
64
+ # Note that opening and closing the client is only required if you want to
65
+ # make several calls under one TCP connection. Otherwise, you can simply
66
+ # call the search methods ({#web}, {#image}, etc.), which call {#open} for
67
+ # you if necessary.
68
+ #
69
+ # @yield
70
+ # If a block is given, the client is closed when the block returns.
71
+ # @return [Object, self]
72
+ # If a block is given, the return value of the block; otherwise, +self+.
73
+ # @raise [StandardError]
74
+ # The client is already open
75
+ #
76
+ def open
77
+ raise "Already open" if open?
78
+
79
+ @session = Net::HTTP.new(HOST, Net::HTTP.https_default_port)
80
+ @session.use_ssl = true
81
+
82
+ begin
83
+ @session.start
84
+ block_given? ? yield : self
85
+ ensure
86
+ close if block_given?
87
+ end
88
+ end
89
+
90
+ # Closes the client. Must be called after {#open} is called without a
91
+ # block.
92
+ # @return [self]
93
+ # @see #open
94
+ #
95
+ def close
96
+ @session.finish if open?
97
+ @session = nil
98
+ self
99
+ end
100
+
101
+ # Whether the client is open
102
+ # @return [Boolean]
103
+ # @see #open
104
+ #
105
+ def open?
106
+ @session && @session.started?
107
+ end
108
+
109
+
110
+ # @!group Searching
111
+
112
+ # @!macro general
113
+ # @param [String] query
114
+ # The query text; supports the
115
+ # {http://msdn.microsoft.com/en-us/library/ff795667.aspx Bing Query Language}
116
+ # @param [Hash<Symbol => Object>] opts
117
+ # @option opts [Integer] :limit
118
+ # The maximum number of results to return
119
+ # @option opts [Integer] :offset
120
+ # The zero-based ordinal of the first result to return
121
+ # @option opts [Adult, Symbol] :adult
122
+ # The level of filtering of sexually explicit content. If omitted, Bing
123
+ # uses the default level for the market.
124
+ # @option opts [Float] :latitude
125
+ # May range from -90 to 90
126
+ # @option opts [Float] :longitude
127
+ # May range from -180 to 180
128
+ # @option opts [String] :market
129
+ # A language tag specifying the market in which to search (e.g.
130
+ # +en-US+). If omitted, Bing infers the market from IP address, etc.
131
+ # @option opts [Boolean] :location_detection (true)
132
+ # Whether to infer location from the query text
133
+ # @raise [ServiceError]
134
+ # The Bing Search service returned an error
135
+ # @raise [StandardError]
136
+ # Invalid argument, unable to parse Bing response, networking error,
137
+ # and other error conditions
138
+ # @see http://msdn.microsoft.com/en-us/library/ff795667.aspx Bing Query Language reference
139
+
140
+ # Searches for web pages
141
+ # @!macro general
142
+ # @option opts [FileType] :file_type
143
+ # Type of file to return
144
+ # @option opts [Boolean] :highlighting (false)
145
+ # Whether to surround query terms in {WebResult#description} with the
146
+ # delimiter {BingSearch::HIGHLIGHT_DELIMITER}.
147
+ # @option opts [Boolean] :host_collapsing (true)
148
+ # Whether to suppress results from the same 'top-level URL'
149
+ # @option opts [Boolean] :query_alterations (true)
150
+ # Whether to alter the query in case of, e.g., supposed spelling errors
151
+ # @return [Array<WebResult>]
152
+ #
153
+ def web(query, opts = {})
154
+ invoke 'Web',
155
+ query,
156
+ opts,
157
+ passthrough_opts: %i(file_type),
158
+ enum_opt_to_module: {file_type: FileType},
159
+ param_name_replacements: {file_type: 'WebFileType'},
160
+ params: {web_search_options: web_search_options_from_opts(opts)}
161
+ end
162
+
163
+ # Searches for images
164
+ # @!macro general
165
+ # @option opts [Integer] :minimum_height
166
+ # In pixels; ANDed with other filters
167
+ # @option opts [Integer] :minimum_width
168
+ # In pixels; ANDed with other filters
169
+ # @option opts [Array<ImageFilter>] :filters
170
+ # Multiple filters are ANDed
171
+ # @return [Array<ImageResult>]
172
+ #
173
+ def image(query, opts = {})
174
+ invoke 'Image',
175
+ query,
176
+ opts,
177
+ param_name_replacements: {filters: 'ImageFilters'},
178
+ params: {filters: image_filters_from_opts(opts)}
179
+ end
180
+
181
+ # Searches for videos
182
+ # @!macro general
183
+ # @option opts [Array<VideoFilter>] :filters
184
+ # Multiple filters are ANDed. At most one duration is allowed.
185
+ # @option opts [VideoSort] :sort
186
+ # @return [Array<VideoResult>]
187
+ #
188
+ def video(query, opts = {})
189
+ invoke 'Video',
190
+ query,
191
+ opts,
192
+ passthrough_opts: %i(filters sort),
193
+ enum_opt_to_module: {filters: VideoFilter, sort: VideoSort},
194
+ param_name_replacements: {filters: 'VideoFilters', sort: 'VideoSortBy'}
195
+ end
196
+
197
+ # Searches for news
198
+ # @!macro general
199
+ # @option opts [Boolean] :highlighting (false)
200
+ # Whether to surround query terms in {NewsResult#description} with the
201
+ # delimiter {BingSearch::HIGHLIGHT_DELIMITER}.
202
+ # @option opts [NewsCategory] :category
203
+ # Only applies in the en-US market. If no news matches the category, Bing
204
+ # returns results from a mix of categories.
205
+ # @option opts [String] :location_override
206
+ # Overrides Bing's location detection. Example: +US.WA+
207
+ # @option opts [NewsSort] :sort
208
+ # @return [Array<NewsResult>]
209
+ #
210
+ def news(query, opts = {})
211
+ invoke 'News',
212
+ query,
213
+ opts,
214
+ passthrough_opts: %i(category location_override sort),
215
+ enum_opt_to_module: {category: NewsCategory, sort: NewsSort},
216
+ param_name_replacements: {category: 'NewsCategory', location_override: 'NewsLocationOverride', sort: 'NewsSortBy'}
217
+ end
218
+
219
+ # Searches for related queries
220
+ # @!macro general
221
+ # @return [Array<RelatedSearchResult>]
222
+ #
223
+ def related_search(query, opts = {})
224
+ invoke 'RelatedSearch', query, opts
225
+ end
226
+ alias_method :related, :related_search
227
+
228
+ # Corrects spelling in the query text
229
+ # @!macro general
230
+ # @return [Array<SpellingSuggestionsResult>]
231
+ #
232
+ def spelling_suggestions(query, opts = {})
233
+ invoke 'SpellingSuggestions', query, opts
234
+ end
235
+ alias_method :spelling, :spelling_suggestions
236
+
237
+ # Searches multiple sources. At most 15 news results are returned by
238
+ # a composite query regardless of the +:limit+ option.
239
+ # @macro general
240
+ # @param [Array<Source>] sources
241
+ # The sources to search
242
+ # @option opts [Boolean] :highlighting (false)
243
+ # Whether to surround query terms in {NewsResult#description} and
244
+ # {WebResult#description} with the delimiter {BingSearch::HIGHLIGHT_DELIMITER}.
245
+ # @option opts [FileType] :web_file_type
246
+ # Type of file to return. Applies to {Source::Web}; also affects
247
+ # {Source::Image} and {Source::Video} if {Source::Web} is specified.
248
+ # @option opts [Boolean] :web_host_collapsing (true)
249
+ # Whether to suppress results from the same 'top-level URL.' Applies to {Source::Web}.
250
+ # @option opts [Boolean] :web_query_alterations (true)
251
+ # Whether to alter the query in case of, e.g., supposed spelling errors. Applies to {Source::Web}.
252
+ # @option opts [Integer] :image_minimum_width
253
+ # In pixels; ANDed with other filters. Applies to {Source::Image}.
254
+ # @option opts [Integer] :image_minimum_height
255
+ # In pixels; ANDed with other image filters. Applies to {Source::Image}.
256
+ # @option opts [Array<ImageFilter>] :image_filters
257
+ # Multiple filters are ANDed. Applies to {Source::Image}.
258
+ # @option opts [Array<VideoFilter>] :video_filters
259
+ # Multiple filters are ANDed. At most one duration is allowed. Applies to {Source::Video}.
260
+ # @option opts [VideoSort] :video_sort
261
+ # Applies to {Source::Video}
262
+ # @option opts [NewsCategory] :news_category
263
+ # Only applies in the en_US market. If no news matches the category, Bing
264
+ # returns results from a mix of categories. Applies to {Source::News}.
265
+ # @option opts [String] :news_location_override
266
+ # Overrides Bing's location detection. Example: +US.WA+. Applies to {Source::News}.
267
+ # @option opts [NewsSort] :news_sort
268
+ # Applies to {Source::News}.
269
+ #
270
+ # @return [CompositeSearchResult]
271
+ #
272
+ def composite(query, sources, opts = {})
273
+ results = invoke('Composite',
274
+ query,
275
+ opts,
276
+ passthrough_opts: %i(
277
+ web_file_type
278
+ video_filters
279
+ video_sort
280
+ news_category
281
+ news_location_override
282
+ news_sort
283
+ ),
284
+ enum_opt_to_module: {
285
+ web_file_type: FileType,
286
+ video_filters: VideoFilter,
287
+ video_sort: VideoSort,
288
+ news_category: NewsCategory,
289
+ news_sort: NewsSort
290
+ },
291
+ param_name_replacements: {
292
+ video_sort: 'VideoSortBy',
293
+ news_sort: 'NewsSortBy'
294
+ },
295
+ params: {
296
+ sources: sources.collect { |source| enum_value(source, Source) },
297
+ web_search_options: web_search_options_from_opts(opts, :web_),
298
+ image_filters: image_filters_from_opts(opts, :image_)
299
+ }
300
+ )
301
+
302
+ results.first
303
+ end
304
+
305
+ private
306
+
307
+ # @param [Hash] opts
308
+ # @param [#to_s] opt_prefix
309
+ # return [Array<String>]
310
+ #
311
+ def web_search_options_from_opts(opts = {}, opt_prefix = nil)
312
+ web_search_options = []
313
+ web_search_options << 'DisableHostCollapsing' if opts["#{opt_prefix}host_collapsing".to_sym] == false
314
+ web_search_options << 'DisableQueryAlterations' if opts["#{opt_prefix}query_alterations".to_sym] == false
315
+ web_search_options
316
+ end
317
+
318
+ # @param [Hash] opts
319
+ # @param [#to_s] opt_prefix
320
+ # @return [Array<String>]
321
+ #
322
+ def image_filters_from_opts(opts = {}, opt_prefix = nil)
323
+ filters = (opts["#{opt_prefix}filters".to_sym] || []).map { |filter| enum_value(filter, ImageFilter) }
324
+
325
+ height = opts["#{opt_prefix}minimum_height".to_sym]
326
+ width = opts["#{opt_prefix}minimum_width".to_sym]
327
+ filters << "Size:Width:#{width}" if width
328
+ filters << "Size:Height:#{height}" if height
329
+
330
+ filters
331
+ end
332
+
333
+ # @param [String] operation
334
+ # The first segment of the invocation URI path (after the base path),
335
+ # e.g. 'Web'
336
+ # @param [String] query
337
+ # The query text
338
+ # @param [Hash<Symbol => Object>] opts
339
+ # The options hash provided by the caller
340
+ # @param [Array<Symbol>] passthrough_opts
341
+ # Keys of the options to copy to the params hash
342
+ # @param [Hash<Symbol => Module>] enum_opt_to_module
343
+ # Maps an enum option key to the module containing the enum's values.
344
+ # Used to translate symbols to enum values. E.g. maps +:web_file_type+
345
+ # to {FileType}.
346
+ # @param [Hash<Symbol => Object>] params
347
+ # Parameters for the invocation
348
+ # @return [Object]
349
+ # @raise [ServiceError]
350
+ # @raise [RuntimeError]
351
+ #
352
+ def invoke(operation, query, opts, passthrough_opts: [], enum_opt_to_module: {}, param_name_replacements: {}, params: {})
353
+ options = []
354
+ options << 'DisableLocationDetection' if opts[:location_detection] == false
355
+ options << 'EnableHighlighting' if opts[:highlighting]
356
+
357
+ # Works around an apparent bug where Bing treats offsets 0 and 1 the same
358
+ offset = opts[:offset] && opts[:offset] + 1
359
+
360
+ opts = opts.each_with_object(Hash.new) do |(key, value), hash|
361
+ module_ = GENERAL_ENUM_OPT_TO_MODULE[key] || enum_opt_to_module[key]
362
+ hash[key] = module_ ? enum_value(value, module_) : value
363
+ end
364
+
365
+ params = params.
366
+ merge(opts.slice(*GENERAL_PASSTHROUGH_OPTS, *passthrough_opts)).
367
+ merge(query: query, offset: offset, options: options, format: :JSON).
368
+ delete_if { |_, v| v.nil? || (v.is_a?(Array) && v.empty?) }
369
+
370
+ params = format_params(replace_param_names(params, param_name_replacements))
371
+ query = URI.encode_www_form(params)
372
+ base_path = web_only ? WEB_ONLY_BASE_PATH : BASE_PATH
373
+
374
+ response = in_session do |session|
375
+ request = Net::HTTP::Get.new("#{base_path}/#{operation}?#{query}")
376
+ request.basic_auth(access_key, access_key)
377
+ session.request request
378
+ end
379
+
380
+ unless response.is_a?(Net::HTTPOK)
381
+ raise ServiceError.new(response.code, response.body)
382
+ end
383
+
384
+ raw = JSON.parse(response.body)
385
+
386
+ unless raw['d'] && raw['d']['results']
387
+ raise "Unexpected response format"
388
+ end
389
+
390
+ parse raw['d']['results']
391
+ end
392
+
393
+ # @yield [Net::HTTP]
394
+ # @return [Net::HTTPResponse]
395
+ #
396
+ def in_session
397
+ if open?
398
+ yield @session
399
+ else
400
+ open { yield @session }
401
+ end
402
+ end
403
+
404
+ # @param [Object] value
405
+ # @param [Module] module_
406
+ # @return [Object]
407
+ #
408
+ def enum_value(value, module_)
409
+ case value
410
+ when Symbol
411
+ enum_from_symbol(value, module_)
412
+ when Array
413
+ value.collect { |element| enum_value(element, module_) }
414
+ else
415
+ value
416
+ end
417
+ end
418
+
419
+ # @param [Symbol] symbol
420
+ # @param [Module] module_
421
+ # @return [Object]
422
+ # @raise [ArgumentError]
423
+ # The module does not contain a constant corresponing to the symbol
424
+ #
425
+ def enum_from_symbol(symbol, module_)
426
+ [symbol.to_s.camelcase, symbol.to_s.upcase].each do |const|
427
+ return module_.const_get(const) if module_.const_defined?(const)
428
+ end
429
+ raise ArgumentError, "#{module_} does not contain a constant corresponding to #{symbol}"
430
+ end
431
+
432
+ # @param [Hash<Symbol => Object>] params
433
+ # @return [Hash<String => Object]
434
+ #
435
+ def replace_param_names(params, replacements)
436
+ params.each_with_object(Hash.new) do |(key, value), hash|
437
+ key = replacements[key] || GENERAL_PARAM_NAME_REPLACEMENTS[key] || key.to_s.camelcase
438
+ hash[key] = value
439
+ end
440
+ end
441
+
442
+ # @param [Hash] params
443
+ # @return [Hash]
444
+ #
445
+ def format_params(params)
446
+ params.each_with_object(Hash.new) do |(key, value), hash|
447
+ hash[key] = format(value)
448
+ end
449
+ end
450
+
451
+ # @param [Object] value
452
+ # @return [String]
453
+ #
454
+ def format(value)
455
+ case value
456
+ when String then format_string(value)
457
+ when Array then format_array(value)
458
+ else value
459
+ end
460
+ end
461
+
462
+ # @param [String] string
463
+ # @return [String]
464
+ #
465
+ def format_string(string)
466
+ "'#{string}'"
467
+ end
468
+
469
+ # @param [Array] array
470
+ # @return [String]
471
+ #
472
+ def format_array(array)
473
+ "'#{array.join '+'}'"
474
+ end
475
+
476
+ # @param [Object] raw
477
+ # @param [Symbol, nil] type
478
+ # @return [Object]
479
+ #
480
+ def parse(raw, type = nil)
481
+ if type
482
+ parse_typed raw, type
483
+ elsif raw.is_a?(Array)
484
+ raw.collect { |element| parse element }
485
+ elsif raw_model?(raw)
486
+ parse_model raw
487
+ else
488
+ raw
489
+ end
490
+ end
491
+
492
+ # @param [Object] raw
493
+ # @param [Symbol] type
494
+ # @return [Object, nil]
495
+ # @raise [ArgumentError]
496
+ #
497
+ def parse_typed(raw, type)
498
+ case type
499
+ when :datetime
500
+ raw.empty? ? nil : DateTime.parse(raw)
501
+ when :integer
502
+ raw.empty? ? nil : Integer(raw)
503
+ else
504
+ raise ArgumentError, "Can't parse value #{raw} of type #{type}"
505
+ end
506
+ end
507
+
508
+ # @param [Object] raw
509
+ # @return [Model]
510
+ #
511
+ def parse_model(raw)
512
+ raw_type = raw['__metadata']['type']
513
+ model = model_class(raw_type).new
514
+ attr_to_type = RAW_MODEL_TYPE_TO_ATTR_TO_TYPE[raw_type] || {}
515
+
516
+ for key, value in raw
517
+ next if key == '__metadata'
518
+ attr = key.underscore.to_sym
519
+ model.set attr, parse(value, attr_to_type[attr])
520
+ end
521
+
522
+ model
523
+ end
524
+
525
+ # @param [Object] raw
526
+ # @return [Boolean]
527
+ #
528
+ def raw_model?(raw)
529
+ raw.is_a?(Hash) && raw['__metadata'] && raw['__metadata']['type']
530
+ end
531
+
532
+ # @param [String] raw_type
533
+ # @return [Class]
534
+ # @raise [ArgumentError]
535
+ #
536
+ def model_class(raw_type)
537
+ unless RAW_MODEL_TYPES.include?(raw_type)
538
+ raise ArgumentError, "Invalid model type: #{raw_type}"
539
+ end
540
+
541
+ case raw_type
542
+ when 'Bing.Thumbnail'
543
+ Image
544
+ when 'SpellResult'
545
+ SpellingSuggestionsResult
546
+ when 'ExpandableSearchResult'
547
+ CompositeSearchResult
548
+ else
549
+ BingSearch.const_get(raw_type)
550
+ end
551
+ end
552
+
553
+
554
+ HOST = 'api.datamarket.azure.com'
555
+ private_constant :HOST
556
+
557
+ BASE_PATH = '/Bing/Search'
558
+ private_constant :BASE_PATH
559
+
560
+ WEB_ONLY_BASE_PATH = '/Bing/SearchWeb'
561
+ private_constant :WEB_ONLY_BASE_PATH
562
+
563
+ GENERAL_PASSTHROUGH_OPTS = %i(limit adult latitude longitude market)
564
+ private_constant :GENERAL_PASSTHROUGH_OPTS
565
+
566
+ GENERAL_ENUM_OPT_TO_MODULE = {adult: Adult}
567
+ private_constant :GENERAL_ENUM_OPT_TO_MODULE
568
+
569
+ GENERAL_PARAM_NAME_REPLACEMENTS = {
570
+ limit: '$top',
571
+ offset: '$skip',
572
+ format: '$format',
573
+ }
574
+ private_constant :GENERAL_PARAM_NAME_REPLACEMENTS
575
+
576
+ RAW_MODEL_TYPES = %w{
577
+ WebResult
578
+ ImageResult
579
+ VideoResult
580
+ NewsResult
581
+ RelatedSearchResult
582
+ SpellResult
583
+ ExpandableSearchResult
584
+ Bing.Thumbnail
585
+ }
586
+ private_constant :RAW_MODEL_TYPES
587
+
588
+ RAW_MODEL_TYPE_TO_ATTR_TO_TYPE = {
589
+ 'ImageResult' => {
590
+ width: :integer,
591
+ height: :integer
592
+ },
593
+ 'NewsResult' => {
594
+ date: :datetime
595
+ },
596
+ 'VideoResult' => {
597
+ run_time: :integer
598
+ },
599
+ 'ExpandableSearchResult' => {
600
+ web_total: :integer,
601
+ web_offset: :integer,
602
+ image_total: :integer,
603
+ image_offset: :integer,
604
+ video_total: :integer,
605
+ video_offset: :integer,
606
+ news_total: :integer,
607
+ news_offset: :integer,
608
+ spelling_suggestions_total: :integer
609
+ }
610
+ }
611
+ private_constant :RAW_MODEL_TYPE_TO_ATTR_TO_TYPE
612
+
613
+ end
614
+ end
@@ -0,0 +1,78 @@
1
+ module BingSearch
2
+ module Adult
3
+ Off = 'Off'
4
+ Moderate = 'Moderate'
5
+ Strict = 'Strict'
6
+ end
7
+
8
+ module FileType
9
+ DOC = 'DOC'
10
+ DWF = 'DWF'
11
+ FEED = 'FEED'
12
+ HTM = 'HTM'
13
+ HTML = 'HTML'
14
+ PDF = 'PDF'
15
+ PPT = 'PPT'
16
+ RTF = 'RTF'
17
+ TEXT = 'TEXT'
18
+ TXT = 'TXT'
19
+ XLS = 'XLS'
20
+ end
21
+
22
+ module ImageFilter
23
+ Small = 'Size:Small'
24
+ Medium = 'Size:Medium'
25
+ Large = 'Size:Large'
26
+ Square = 'Aspect:Square'
27
+ Wide = 'Aspect:Wide'
28
+ Tall = 'Aspect:Tall'
29
+ Color = 'Color:Color'
30
+ Monochrome = 'Color:Monochrome'
31
+ Photo = 'Style:Photo'
32
+ Graphics = 'Style:Graphics'
33
+ Face = 'Face:Face'
34
+ Portrait = 'Face:Portrait'
35
+ OtherFace = 'Face:Other'
36
+ end
37
+
38
+ module NewsCategory
39
+ Business = 'rt_Business'
40
+ Entertainment = 'rt_Entertainment'
41
+ Health = 'rt_Health'
42
+ Politics = 'rt_Politics'
43
+ Sports = 'rt_Sports'
44
+ US = 'rt_US'
45
+ World = 'rt_World'
46
+ ScienceAndTechnology = 'rt_ScienceAndTechnology'
47
+ end
48
+
49
+ module NewsSort
50
+ Date = 'Date'
51
+ Relevance = 'Relevance'
52
+ end
53
+
54
+ module Source
55
+ Web = 'Web'
56
+ Image = 'Image'
57
+ Video = 'Video'
58
+ News = 'News'
59
+ SpellingSuggestions = 'Spell'
60
+ RelatedSearch = 'RelatedSearch'
61
+ end
62
+
63
+ module VideoFilter
64
+ Short = 'Duration:Short'
65
+ Medium = 'Duration:Medium'
66
+ Long = 'Duration:Long'
67
+ StandardAspect = 'Aspect:Standard'
68
+ Widescreen = 'Aspect:Widescreen'
69
+ LowResolution = 'Resolution:Low'
70
+ MediumResolution = 'Resolution:Medium'
71
+ HighResolution = 'Resolution:High'
72
+ end
73
+
74
+ module VideoSort
75
+ Date = 'Date'
76
+ Relevance = 'Relevance'
77
+ end
78
+ end
@@ -0,0 +1,23 @@
1
+ module BingSearch
2
+
3
+ class ServiceError < StandardError
4
+ # The error code returned by Bing
5
+ # @return [String]
6
+ attr_reader :code
7
+
8
+ # @param [String] code
9
+ # The error code returned by Bing
10
+ # @param [String] message
11
+ #
12
+ def initialize(code, message = nil)
13
+ super(message)
14
+ @code = code
15
+ end
16
+
17
+ # @return [String]
18
+ #
19
+ def to_s
20
+ "Bing error #{code}: #{super}"
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,255 @@
1
+ module BingSearch
2
+
3
+ class Model
4
+ # @param [Hash] attrs
5
+ def initialize(attrs = {})
6
+ attrs.each do |k, v|
7
+ public_send "#{k}=", v
8
+ end
9
+ end
10
+
11
+ # Sets an attribute via a public instance method on the receiver or its
12
+ # ancestors up to but not including Object
13
+ # @param [Symbol] attr
14
+ # @param value
15
+ # @return [self]
16
+ # @raise [ArgumentError]
17
+ # No public setter for +attr+ on the receiver or its ancestors up to
18
+ # Object
19
+ #
20
+ def set(attr, value)
21
+ setter = "#{attr}=".to_sym
22
+
23
+ self.class.attr_methods.include?(setter) ?
24
+ public_send(setter, value) :
25
+ raise(ArgumentError, "Can't set attr #{attr} of #{self}")
26
+ end
27
+
28
+ private
29
+
30
+ def self.attr_methods
31
+ @attr_methods ||= model_ancestors.reduce([]) do |memo, class_|
32
+ memo + class_.public_instance_methods(false)
33
+ end
34
+ end
35
+
36
+ def self.model_ancestors
37
+ ancestors.select { |ancestor| ancestor < Model }
38
+ end
39
+ end
40
+
41
+ class Result < Model
42
+ # @return [String]
43
+ # A universally unique identifier (UUID)
44
+ attr_accessor :id
45
+ end
46
+
47
+ class WebResult < Result
48
+ # @return [String]
49
+ attr_accessor :title
50
+
51
+ # The summary displayed below the title in bing.com search results
52
+ # @return [String]
53
+ attr_accessor :description
54
+ alias_method :summary, :description
55
+
56
+ # URL to display to the user. Omits the scheme if HTTP.
57
+ # @return [String]
58
+ attr_accessor :display_url
59
+
60
+ # Full URL of the result, including the scheme.
61
+ # @return [String]
62
+ attr_accessor :url
63
+ end
64
+
65
+ class ImageResult < Result
66
+ # @return [String]
67
+ attr_accessor :title
68
+
69
+ # URL of the image
70
+ # @return [String]
71
+ attr_accessor :media_url
72
+ alias_method :url, :media_url
73
+
74
+ # URL of the website that contains the image
75
+ # @return [String]
76
+ attr_accessor :source_url
77
+
78
+ # URL to display to the user. Omits the scheme if HTTP.
79
+ # @return [String]
80
+ attr_accessor :display_url
81
+
82
+ # In pixels, if available
83
+ # @return [Integer, nil]
84
+ attr_accessor :width
85
+
86
+ # In pixels, if available
87
+ # @return [Integer, nil]
88
+ attr_accessor :height
89
+
90
+ # In bytes, if available
91
+ # @return [Integer, nil]
92
+ attr_accessor :file_size
93
+
94
+ # {http://en.wikipedia.org/wiki/Internet_media_type Internet media type} (MIME type) of the image, if available
95
+ # @return [String]
96
+ attr_accessor :content_type
97
+ alias_method :media_type, :content_type
98
+
99
+ # @return [Image]
100
+ attr_accessor :thumbnail
101
+ end
102
+
103
+ class VideoResult < Result
104
+ # @return [String]
105
+ attr_accessor :title
106
+
107
+ # URL of the video, often a web page containing the video
108
+ # @return [String]
109
+ attr_accessor :media_url
110
+ alias_method :url, :media_url
111
+
112
+ # URL of a Bing page that displays the video
113
+ # @return [String]
114
+ attr_accessor :display_url
115
+
116
+ # Duration of the video in milliseconds, if available
117
+ # @return [Integer, nil]
118
+ attr_accessor :run_time
119
+ alias_method :duration, :run_time
120
+
121
+ # @return [Image]
122
+ attr_accessor :thumbnail
123
+ end
124
+
125
+ class NewsResult < Result
126
+ # @return [String]
127
+ attr_accessor :title
128
+ alias_method :headline, :title
129
+
130
+ # URL of the article
131
+ # @return [String]
132
+ attr_accessor :url
133
+
134
+ # Organization responsible for the article
135
+ # @return [String]
136
+ attr_accessor :source
137
+
138
+ # Sample of the article
139
+ # @return [String]
140
+ attr_accessor :description
141
+
142
+ # Date on which the article was indexed
143
+ # @return [Date]
144
+ attr_accessor :date
145
+ end
146
+
147
+ class RelatedSearchResult < Result
148
+ # The query text of the related search
149
+ # @return [String]
150
+ attr_accessor :title
151
+ alias_method :query, :title
152
+
153
+ # The URL of the Bing results page for the related search
154
+ # @return [String]
155
+ attr_accessor :bing_url
156
+ end
157
+
158
+ class SpellingSuggestionsResult < Result
159
+ # The suggested spelling
160
+ # @return [String]
161
+ attr_accessor :value
162
+ alias_method :suggestion, :value
163
+ end
164
+
165
+ class CompositeSearchResult < Result
166
+ # @!group Instance Attributes for Results
167
+
168
+ # @return [Array<WebResult>]
169
+ attr_accessor :web
170
+
171
+ # @return [Array<ImageResult>]
172
+ attr_accessor :image
173
+
174
+ # @return [Array<VideoResult>]
175
+ attr_accessor :video
176
+
177
+ # @return [Array<NewsResult>]
178
+ attr_accessor :news
179
+
180
+ # @return [Array<RelatedSearchResult>]
181
+ attr_accessor :related_search
182
+
183
+ # @return [Array<SpellingSuggestionsResult>]
184
+ attr_accessor :spelling_suggestions
185
+
186
+ # @!endgroup
187
+
188
+ # The number of web results in the Bing index
189
+ # @return [Integer, nil]
190
+ attr_accessor :web_total
191
+
192
+ # The ordinal of the first web result
193
+ # @return [Integer, nil]
194
+ attr_accessor :web_offset
195
+
196
+ # The number of image results in the Bing index
197
+ # @return [Integer, nil]
198
+ attr_accessor :image_total
199
+
200
+ # The ordinal of the first image result
201
+ # @return [Integer, nil]
202
+ attr_accessor :image_offset
203
+
204
+ # The number of video results in the Bing index
205
+ # @return [Integer, nil]
206
+ attr_accessor :video_total
207
+
208
+ # The ordinal of the first video result
209
+ # @return [Integer, nil]
210
+ attr_accessor :video_offset
211
+
212
+ # The number of news results in the Bing index
213
+ # @return [Integer, nil]
214
+ attr_accessor :news_total
215
+
216
+ # The ordinal of the first news result
217
+ # @return [Integer, nil]
218
+ attr_accessor :news_offset
219
+
220
+ # The number of spelling suggestions in the Bing index
221
+ # @return [Integer, nil]
222
+ attr_accessor :spelling_suggestions_total
223
+
224
+ # The query text after spelling errors have been corrected
225
+ # @return [String]
226
+ attr_accessor :altered_query
227
+
228
+ # Query text that forces the original query, preventing any alterations in {#altered_query}
229
+ # @return [String]
230
+ attr_accessor :alteration_override_query
231
+ end
232
+
233
+ class Image < Model
234
+ # URL of the image
235
+ # @return [String]
236
+ attr_accessor :media_url
237
+ alias_method :url, :media_url
238
+
239
+ # {http://en.wikipedia.org/wiki/Internet_media_type Internet media type} (MIME type) of the image, if available
240
+ # @return [String]
241
+ attr_accessor :content_type
242
+
243
+ # In pixels, if available
244
+ # @return [Integer, nil]
245
+ attr_accessor :width
246
+
247
+ # In pixels, if available
248
+ # @return [Integer, nil]
249
+ attr_accessor :height
250
+
251
+ # In bytes, if available
252
+ # @return [Integer, nil]
253
+ attr_accessor :file_size
254
+ end
255
+ end
@@ -0,0 +1,3 @@
1
+ module BingSearch
2
+ VERSION = '1.0.0'
3
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bing-search
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Jonah Burke
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-01-22 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 4.2.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 4.2.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.7'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.7'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: yard
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: 0.8.7
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: 0.8.7
69
+ description:
70
+ email:
71
+ - jonah@jonahb.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - LICENSE.txt
77
+ - README.md
78
+ - lib/bing-search.rb
79
+ - lib/bing-search/client.rb
80
+ - lib/bing-search/enums.rb
81
+ - lib/bing-search/errors.rb
82
+ - lib/bing-search/models.rb
83
+ - lib/bing-search/version.rb
84
+ homepage: http://github.com/jonahb/bing-search
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '2.0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.2.2
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: A Ruby client for the Bing Search API
108
+ test_files: []
109
+ has_rdoc: