google-custom_search 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,57 @@
1
+ module Google
2
+ module CustomSearch
3
+ class Config
4
+ attr_accessor :path
5
+ attr_accessor :environment
6
+
7
+ [:key, :cx, :cse_id, :cref, :service].each do |key|
8
+ attr_writer key
9
+ define_method(key) do
10
+ instance_variable_get("@#{key}".to_sym) || read(key)
11
+ end
12
+ end
13
+
14
+ def service_type
15
+ eval("#{self.service.upcase}::Service")
16
+ end
17
+
18
+ private
19
+
20
+ def read(key)
21
+ env_config[key.to_s] || raise("Key '#{key}' was not found in your config file #{path}. Alternatives: #{env_config.keys.join(',')}")
22
+ end
23
+
24
+ def env_config
25
+ all_config[environment] || raise("No key for environment '#{environment}' found in #{path}")
26
+ end
27
+
28
+ def all_config
29
+ return @config if @config
30
+ [:path, :environment].each do |param|
31
+ raise "Please set Google::CustomSearch.config.#{param}" unless self.send(param)
32
+ end
33
+
34
+ raise "Config file does not exist at #{path}" unless File.exists?(path)
35
+ @config = YAML.load_file(path)
36
+ end
37
+ end
38
+
39
+ class << self
40
+ attr_accessor :config
41
+
42
+ def configure
43
+ yield config
44
+ end
45
+
46
+ end
47
+
48
+ self.config = Config.new
49
+ end
50
+ end
51
+
52
+ if defined?(Rails)
53
+ Google::CustomSearch.configure do |config|
54
+ config.path = "#{Rails.root}/config/google.yml"
55
+ config.environment = Rails.env
56
+ end
57
+ end
@@ -0,0 +1,102 @@
1
+ require 'json'
2
+
3
+ module Google
4
+ module CustomSearch
5
+
6
+ module JSON
7
+
8
+ class Results
9
+ include Enumerable
10
+
11
+ def initialize(response)
12
+ @data = ::JSON.parse(response)
13
+ end
14
+
15
+ def each(&block)
16
+ items.each(&block)
17
+ end
18
+
19
+ def empty?
20
+ items.empty?
21
+ end
22
+
23
+ def length
24
+ items.length
25
+ end
26
+
27
+ def next_page
28
+ Page.new(@data['queries']['nextPage'] || @data['queries']['request'])
29
+ end
30
+
31
+ def current_page
32
+ Page.new(@data['queries']['request'])
33
+ end
34
+
35
+ def previous_page
36
+ Page.new(@data['queries']['previousPage'] || @data['queries']['request'])
37
+ end
38
+
39
+ private
40
+
41
+ def items
42
+ @items ||= (@data['items'] || []).map do |item|
43
+ Result.new(item)
44
+ end
45
+ end
46
+
47
+ class Page
48
+ def initialize(data)
49
+ @data = data[0]
50
+ end
51
+
52
+ def start_index
53
+ @data['startIndex']
54
+ end
55
+
56
+ def to_s
57
+ "results #{start_index}-#{end_index} of #{total}"
58
+ end
59
+
60
+ def ==(other)
61
+ other.start_index == start_index
62
+ end
63
+
64
+ private
65
+
66
+ def total
67
+ @data['totalResults']
68
+ end
69
+
70
+ def end_index
71
+ start_index + @data['count'] - 1
72
+ end
73
+ end
74
+
75
+ end
76
+
77
+ class Result
78
+ def initialize(data)
79
+ @data = data
80
+ end
81
+
82
+ def title
83
+ @data['title'] || ''
84
+ end
85
+
86
+ def uri
87
+ @data['link']
88
+ end
89
+
90
+ def content
91
+ @data['htmlSnippet']
92
+ end
93
+
94
+ def site
95
+ @data['displayLink']
96
+ end
97
+ end
98
+
99
+ end
100
+
101
+ end
102
+ end
@@ -0,0 +1,38 @@
1
+ require 'google/custom_search/json/results'
2
+
3
+ module Google
4
+ module CustomSearch
5
+ module JSON
6
+
7
+ class Service
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @resource = RestClient::Resource.new('https://www.googleapis.com/customsearch/v1')
12
+ end
13
+
14
+ def request(query_string, start_index)
15
+ @resource.get(:params => params(query_string, start_index)) do |response, request, result|
16
+ unless response.code == 200
17
+ raise BadRequestError, "Unable to fetch results from Google: #{response}"
18
+ end
19
+ Results.new(response)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def params(query_string, start_index)
26
+ {
27
+ :q => query_string,
28
+ :start => start_index,
29
+ :key => @config.key,
30
+ :cref => @config.cref,
31
+ }
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,35 @@
1
+ require 'google/custom_search/config'
2
+ require 'google/custom_search/json/service'
3
+ require 'google/custom_search/xml/service'
4
+
5
+ module Google
6
+ module CustomSearch
7
+
8
+ class BadRequestError < StandardError
9
+ end
10
+
11
+ class ConfigurationError < StandardError
12
+ end
13
+
14
+ class Query
15
+
16
+ def initialize(query_string, start_index, config = Google::CustomSearch.config)
17
+ @query_string = query_string
18
+ @start_index = start_index
19
+ @service = config.service_type.new(config)
20
+ end
21
+
22
+ def results
23
+ attempts = 5
24
+ begin
25
+ @service.request(@query_string, @start_index)
26
+ rescue BadRequestError
27
+ attempts -= 1
28
+ retry if attempts > 0
29
+ raise
30
+ end
31
+ end
32
+
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,95 @@
1
+ require 'json'
2
+ require 'nokogiri'
3
+
4
+ module Google
5
+ module CustomSearch
6
+
7
+ module XML
8
+
9
+ class Results
10
+ include Enumerable
11
+
12
+ def initialize(response)
13
+ @doc = Nokogiri::XML.parse(response)
14
+ end
15
+
16
+ def each(&block)
17
+ items.each(&block)
18
+ end
19
+
20
+ def empty?
21
+ items.empty?
22
+ end
23
+
24
+ def length
25
+ items.length
26
+ end
27
+
28
+ def next_page
29
+ return current_page unless @doc.search('//RES/NB').any?
30
+ Page.new(current_page.end_index + 1, current_page.end_index + 11, 10)
31
+ end
32
+
33
+ def current_page
34
+ Page.from_xml(@doc.search('//RES').first)
35
+ end
36
+
37
+ def previous_page
38
+ return current_page if current_page.start_index == 1
39
+ Page.new(current_page.start_index - 10, current_page.start_index, 10)
40
+ end
41
+
42
+ private
43
+
44
+ def items
45
+ @items ||= @doc.search('//RES/R').map do |node|
46
+ Result.new(node)
47
+ end
48
+ end
49
+
50
+ class Page < Struct.new(:start_index, :end_index, :total)
51
+ def self.from_xml(node)
52
+ new(
53
+ node.attributes['SN'].text.to_i,
54
+ node.attributes['EN'].text.to_i,
55
+ node.search('M').text.to_i)
56
+ end
57
+
58
+ def to_s
59
+ "results #{start_index}-#{end_index} of #{total}"
60
+ end
61
+
62
+ def ==(other)
63
+ other.start_index == start_index
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ class Result
71
+ def initialize(node)
72
+ @node = node
73
+ end
74
+
75
+ def title
76
+ @node.search('T').text
77
+ end
78
+
79
+ def uri
80
+ @node.search('U').text
81
+ end
82
+
83
+ def content
84
+ @node.search('S').text
85
+ end
86
+
87
+ def site
88
+ URI.parse(@node.search('U').text).host
89
+ end
90
+ end
91
+
92
+ end
93
+
94
+ end
95
+ end
@@ -0,0 +1,38 @@
1
+ require 'google/custom_search/xml/results'
2
+
3
+ module Google
4
+ module CustomSearch
5
+ module XML
6
+
7
+ class Service
8
+
9
+ def initialize(config)
10
+ @config = config
11
+ @resource = RestClient::Resource.new('http://www.google.com/cse')
12
+ end
13
+
14
+ def request(query_string, start_index)
15
+ @resource.get(:params => params(query_string, start_index)) do |response, request, result|
16
+ unless response.code == 200
17
+ raise BadRequestError, "Unable to fetch results from Google: #{response}"
18
+ end
19
+ Results.new(response)
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ def params(query_string, start_index)
26
+ {
27
+ :q => query_string,
28
+ :output => 'xml_no_dtd',
29
+ :start => start_index - 1,
30
+ :cx => @config.cx,
31
+ }
32
+ end
33
+
34
+ end
35
+
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ require 'rest-client'
2
+ require 'json'
3
+ require 'google/custom_search/query'
@@ -0,0 +1,9 @@
1
+ require 'spec_helper'
2
+
3
+ module Google::CustomSearch::JSON
4
+ describe Result do
5
+ it "returns an empty string where there is no title" do
6
+ Result.new({}).title.should == ''
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,163 @@
1
+ require 'spec_helper'
2
+
3
+ module Google
4
+ describe CustomSearch do
5
+ let(:config) do
6
+ stub \
7
+ :cref => 'https://github.com/mattwynne/google-custom_search/blob/master/spec/fixtures/mcht-only.xml',
8
+ :key => 'AIzaSyA-AayUZh6S5mGMmja3pt2gfpsncLiwqN8',
9
+ :service_type => CustomSearch::JSON::Service
10
+ end
11
+
12
+ def search_for(query_string, options = {})
13
+ start_index = options[:start_index] || 1
14
+ query = CustomSearch::Query.new(query_string, start_index, config)
15
+ query.results
16
+ end
17
+
18
+ describe "retry on error" do
19
+
20
+ context "when Google returns a 400 bad request error" do
21
+
22
+ before(:each) do
23
+ stub_request(:any, /.*/).to_return(
24
+ :status => [400, "Bad Request"],
25
+ :body => "A helpful message"
26
+ )
27
+ end
28
+
29
+ it "raises an error" do
30
+ expect { search_for 'foo' }.to raise_error(CustomSearch::BadRequestError)
31
+ end
32
+
33
+ it "returns the body of the response in the error message" do
34
+ begin
35
+ search_for 'foo'
36
+ rescue => error
37
+ error.to_s.should =~ /A helpful message/
38
+ end
39
+ end
40
+
41
+ end
42
+
43
+ context "when Google returns a 500 bad request error for the first four requests but subsequent requests are OK" do
44
+
45
+ before(:each) do
46
+ stub_request(:any, /.*/).to_return(
47
+ { :status => [500, "Bad Request"] },
48
+ { :status => [500, "Bad Request"] },
49
+ { :status => [500, "Bad Request"] },
50
+ { :status => [500, "Bad Request"] },
51
+ { :status => [200, "OK"], :body => '[]' }
52
+ )
53
+ end
54
+
55
+ it "does not raise an error" do
56
+ expect { search_for 'foo' }.to_not raise_error(CustomSearch::BadRequestError)
57
+ end
58
+
59
+ end
60
+ end
61
+
62
+ {
63
+ :json => {
64
+ :service_type => CustomSearch::JSON::Service,
65
+ :cref => 'https://raw.github.com/mattwynne/google-custom_search/master/spec/fixtures/json_api_annotations.xml',
66
+ :key => 'AIzaSyA-AayUZh6S5mGMmja3pt2gfpsncLiwqN8'
67
+ },
68
+ :xml => {
69
+ :service_type => CustomSearch::XML::Service,
70
+ :cx => '003087164461061609361:-u1ua6laowa'
71
+ }
72
+ }.each do |service, config|
73
+
74
+ context "using the #{service} service" do
75
+ let(:config) { stub config }
76
+ before { config.stub(:service => service) }
77
+
78
+ context "searching for something that exists in the list" do
79
+ let(:results) { search_for 'kath morgan' }
80
+
81
+ it "returns results" do
82
+ results.any?.should be_true
83
+ end
84
+
85
+ it "returns the expected result" do
86
+ results.first.content.should =~ /01270 275215/
87
+ # that's Kath's phone number
88
+ end
89
+
90
+ it 'returns the expected site' do
91
+ results.first.site.should == 'www.mcht.nhs.uk'
92
+ end
93
+
94
+ end
95
+
96
+ context "searching for something that doesn't exist" do
97
+ let(:results) { search_for 'bobbins' }
98
+
99
+ it "returns no results" do
100
+ results.should be_empty
101
+ end
102
+ end
103
+
104
+ describe "#next_page" do
105
+
106
+ context "a search that returns multiple pages of results" do
107
+ let(:results) { search_for 'trust' }
108
+
109
+ it "has a start_index of 11" do
110
+ results.next_page.start_index.should == 11
111
+ end
112
+ end
113
+
114
+ context "a search for the second of multiple pages" do
115
+ let(:results) { search_for 'trust', :start_index => 11 }
116
+
117
+ it "has a start_index of 21" do
118
+ results.next_page.start_index.should == 21
119
+ end
120
+ end
121
+
122
+ context "a search that returns one page of results" do
123
+ let(:results) { search_for 'kath morgan' }
124
+
125
+ it "returns 1, pointing you back to the same page of results" do
126
+ results.next_page.start_index.should == 1
127
+ end
128
+
129
+ it "is equal to the #current_page" do
130
+ results.next_page.should == results.current_page
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "#current_page" do
136
+ context "a search for the second of multiple pages" do
137
+ let(:results) { search_for 'trust', :start_index => 11 }
138
+
139
+ it "has a start_index of 11" do
140
+ results.current_page.start_index.should == 11
141
+ end
142
+
143
+ it "converts to a summary string" do
144
+ results.current_page.to_s.should =~ /results 11-20 of \d+/
145
+ end
146
+ end
147
+ end
148
+
149
+ describe "#previous_page" do
150
+ context "a search for the second of multiple pages" do
151
+ let(:results) { search_for 'trust', :start_index => 11 }
152
+
153
+ it "has a start_index of 1" do
154
+ results.previous_page.start_index.should == 1
155
+ end
156
+
157
+ end
158
+ end
159
+
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,12 @@
1
+ RSpec.configure do |config|
2
+ config.mock_with :rspec
3
+ end
4
+
5
+ require 'webmock/rspec'
6
+ WebMock.allow_net_connect!
7
+
8
+ require 'google/custom_search'
9
+ Google::CustomSearch.configure do |config|
10
+ config.path = File.dirname(__FILE__) + '/fixtures/config/google.yml'
11
+ config.environment = 'test'
12
+ end
metadata ADDED
@@ -0,0 +1,109 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: google-custom_search
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Matt Wynne
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-12-02 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: rest-client
16
+ requirement: &2152308960 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.6'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2152308960
25
+ - !ruby/object:Gem::Dependency
26
+ name: nokogiri
27
+ requirement: &2152307820 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ~>
31
+ - !ruby/object:Gem::Version
32
+ version: '1.5'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: *2152307820
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &2152306800 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - =
42
+ - !ruby/object:Gem::Version
43
+ version: '2.7'
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2152306800
47
+ - !ruby/object:Gem::Dependency
48
+ name: webmock
49
+ requirement: &2152305660 !ruby/object:Gem::Requirement
50
+ none: false
51
+ requirements:
52
+ - - =
53
+ - !ruby/object:Gem::Version
54
+ version: '1.7'
55
+ type: :development
56
+ prerelease: false
57
+ version_requirements: *2152305660
58
+ - !ruby/object:Gem::Dependency
59
+ name: guard-rspec
60
+ requirement: &2152305060 !ruby/object:Gem::Requirement
61
+ none: false
62
+ requirements:
63
+ - - ! '>='
64
+ - !ruby/object:Gem::Version
65
+ version: '0'
66
+ type: :development
67
+ prerelease: false
68
+ version_requirements: *2152305060
69
+ description: Ruby library for querying Google's Custom Search APIs.
70
+ email: matt@mattwynne.net
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - lib/google/custom_search/config.rb
76
+ - lib/google/custom_search/json/results.rb
77
+ - lib/google/custom_search/json/service.rb
78
+ - lib/google/custom_search/query.rb
79
+ - lib/google/custom_search/xml/results.rb
80
+ - lib/google/custom_search/xml/service.rb
81
+ - lib/google/custom_search.rb
82
+ - spec/lib/google/custom_search/json/result_spec.rb
83
+ - spec/lib/google/custom_search_spec.rb
84
+ - spec/spec_helper.rb
85
+ homepage: https://github.com/mattwynne/gooogle-custom_search
86
+ licenses: []
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ none: false
93
+ requirements:
94
+ - - ! '>='
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ none: false
99
+ requirements:
100
+ - - ! '>='
101
+ - !ruby/object:Gem::Version
102
+ version: '0'
103
+ requirements: []
104
+ rubyforge_project:
105
+ rubygems_version: 1.8.10
106
+ signing_key:
107
+ specification_version: 3
108
+ summary: Interface for Google's Custom Search APIs
109
+ test_files: []