google-custom_search 0.0.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,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: []