ratebeer 0.1.0 → 0.1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 45bf83b47740dbbdbed33401175d33db8917f672
4
- data.tar.gz: f5eaa7e062b02b8391d094e8395c57265b3f92e2
3
+ metadata.gz: cf9d9f5539a98733d67f57195ea03c9517b82278
4
+ data.tar.gz: 79d95ea76346b5e93b1bb73ac27ca088c746d4f9
5
5
  SHA512:
6
- metadata.gz: fc34adee973d864596e83ee5570c5540a8ff0216c847fc2b9dd2a8b5e4121240ec13a443dfb346b69768be0e9ea0898b35eed18df8349879b0dbd221a3dda6f5
7
- data.tar.gz: 3bcf0702e0cad27ef4b2a08da5000b8465043c30d11934d49191078e3df001c66900a09393826f9d42075e1d3381257f146a829bb6c9a3d1f21e5732f8878def
6
+ metadata.gz: 6bcd1a25689ec578662d50f7bfafd271a525eb548f4eebb00724184c92ceb055a67346c5ca89f46a782d08b3c93f24c0ccda8d230edef609046df20e836d2eab
7
+ data.tar.gz: aeb41234258c933f21c410c77f9d3d96f8b38c3f3ca1f532624d27b2b4f51d456c88e29f3e2ad68e6b18dbc703ed357fab49631e296fb3e19d7fcb1262b9f7e9
data/Gemfile CHANGED
@@ -1,5 +1,5 @@
1
1
  source 'https://rubygems.org'
2
- ruby '2.3.0'
2
+ ruby '2.4.0'
3
3
 
4
4
  gemspec
5
5
 
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- ratebeer (0.0.8)
4
+ ratebeer (0.1.0)
5
5
  i18n
6
6
  nokogiri
7
7
 
@@ -51,7 +51,7 @@ DEPENDENCIES
51
51
  rspec
52
52
 
53
53
  RUBY VERSION
54
- ruby 2.3.0p0
54
+ ruby 2.4.0p0
55
55
 
56
56
  BUNDLED WITH
57
- 1.12.5
57
+ 1.14.4
data/lib/ratebeer.rb CHANGED
@@ -13,7 +13,7 @@ module RateBeer
13
13
  # @return [RateBeer::Beer] beer with passed ID#
14
14
  #
15
15
  def beer(id, name = nil)
16
- Beer.new(id, name: name)
16
+ Beer::Beer.new(id, name: name)
17
17
  end
18
18
 
19
19
  # Create new brewery instance, using ID and name passed as arguments.
@@ -23,7 +23,7 @@ module RateBeer
23
23
  # @return [RateBeer::Brewery] brewery with passed ID#
24
24
  #
25
25
  def brewery(id, name = nil)
26
- Brewery.new(id, name: name)
26
+ Brewery::Brewery.new(id, name: name)
27
27
  end
28
28
 
29
29
  # Create new style instance, using ID and name passed as arguments.
data/lib/ratebeer/beer.rb CHANGED
@@ -1,193 +1,154 @@
1
- require_relative "brewery"
2
- require_relative "review"
3
- require_relative "style"
4
- require_relative "scraping"
5
- require_relative "urls"
1
+ # frozen_string_literal: true
6
2
 
7
- module RateBeer
8
- class Beer
9
- # Each key represents an item of data accessible for each beer, and defines
10
- # dynamically a series of methods for accessing this data.
11
- #
12
- def self.data_keys
13
- [:name,
14
- :brewery,
15
- :style,
16
- :glassware,
17
- :availability,
18
- :abv,
19
- :calories,
20
- :description,
21
- :retired,
22
- :rating]
23
- end
24
-
25
- include RateBeer::Scraping
26
- include RateBeer::URLs
27
-
28
- # CSS selector for the root element containing beer information.
29
- ROOT_SELECTOR = '#container table'.freeze
30
-
31
- # CSS selector for the beer information element.
32
- INFO_SELECTOR = 'table'.freeze
3
+ require_relative 'beer/alias'
4
+ require_relative 'brewery'
5
+ require_relative 'review'
6
+ require_relative 'style'
7
+ require_relative 'scraping'
8
+ require_relative 'urls'
33
9
 
34
- # Create RateBeer::Beer instance.
35
- #
36
- # Requires the RateBeer ID# for the beer in question.
10
+ module RateBeer
11
+ module Beer
12
+ # The Beer class.
37
13
  #
38
- # @param [Integer, String] id ID# of beer to retrieve
39
- # @param [String] name Name of the beer to which ID# relates if known
40
- # @param [hash] options Options hash for entity created
14
+ # This class represents one beer found on RateBeer.com, and provides
15
+ # functionality for obtaining information from the site.
41
16
  #
42
- def initialize(id, name: nil, **options)
43
- super
44
- end
45
-
46
- def doc
47
- unless instance_variable_defined?("@doc")
48
- @doc = noko_doc(URI.join(BASE_URL, beer_url(id)))
49
- validate_beer
50
- redirect_if_aliased
17
+ # The key functionality is defined in the self.data_keys method, each key
18
+ # representing a piece of accessible data.
19
+ class Beer
20
+ # Each key represents an item of data accessible for each beer. The
21
+ # included scraping module defines dynamically a series of methods for
22
+ # accessing this data.
23
+ #
24
+ def self.data_keys
25
+ [:name,
26
+ :brewery,
27
+ :style,
28
+ :glassware,
29
+ :abv,
30
+ :description,
31
+ :retired,
32
+ :rating]
51
33
  end
52
- @doc
53
- end
54
34
 
55
- def root
56
- @root ||= doc.at_css(ROOT_SELECTOR)
57
- end
58
-
59
- def info_root
60
- @info_root ||= root.at_css(INFO_SELECTOR)
61
- end
35
+ include RateBeer::Beer
36
+ include RateBeer::Scraping
37
+ include RateBeer::URLs
38
+
39
+ # CSS selector for the root element containing beer information.
40
+ ROOT_SELECTOR = '#container table'
41
+
42
+ # CSS selector for the beer information element.
43
+ INFO_SELECTOR = 'table'
44
+
45
+ # Create RateBeer::Beer instance.
46
+ #
47
+ # Requires the RateBeer ID# for the beer in question.
48
+ #
49
+ # @param [Integer, String] id ID# of beer to retrieve
50
+ # @param [String] name Name of the beer to which ID# relates if known
51
+ # @param [hash] options Options hash for entity created
52
+ #
53
+ def initialize(id, name: nil, **options)
54
+ super
55
+ end
62
56
 
63
- # Return reviews of this beer.
64
- #
65
- def reviews(order: :most_recent, limit: 10)
66
- Review.retrieve(self, order: order, limit: limit)
67
- end
57
+ def doc
58
+ unless instance_variable_defined?('@doc')
59
+ @doc = noko_doc(URI.join(BASE_URL, beer_url(id)))
60
+ validate_beer
61
+ scrape_name # Name must be scraped before any possible redirection.
62
+ @doc = redirect_if_alias(@doc) || @doc
63
+ end
64
+ @doc
65
+ end
68
66
 
69
- private
67
+ def root
68
+ @root ||= doc.at_css(ROOT_SELECTOR)
69
+ end
70
70
 
71
- # Redirects this beer to the "proper" beer page if it represents an alias
72
- # of another beer.
73
- #
74
- # This method overwrites the value of @doc, so that this will scrape the
75
- # details of the proper beer, and not the alias.
76
- def redirect_if_aliased
77
- # retrieve details of the proper beer instead.
78
- alias_pattern = /Also known as(.|\n)*Proceed to the aliased beer\.{3}/
79
- local_root = doc.at_css(ROOT_SELECTOR)
80
- if local_root.css('tr')[1].css('div div').text =~ alias_pattern
81
- scrape_name # Set the name to the original, non-aliased beer.
82
- alias_node = local_root.css('tr')[1]
83
- .css('div div')
84
- .css('a')
85
- .first
86
- @alias_id = alias_node['href'].split('/').last.to_i
87
- @doc = noko_doc(URI.join(BASE_URL, beer_url(@alias_id)))
71
+ def info_root
72
+ @info_root ||= root.at_css(INFO_SELECTOR)
88
73
  end
89
- end
90
74
 
91
- def validate_beer
92
- error_message = 'we didn\'t find this beer'
93
- if name == error_message
94
- raise PageNotFoundError.new("Beer not found - #{id}")
75
+ # Return reviews of this beer.
76
+ #
77
+ def reviews(order: :most_recent, limit: 10)
78
+ Review.retrieve(self, order: order, limit: limit)
95
79
  end
96
- end
97
80
 
98
- def scrape_name
99
- @name ||= fix_characters(doc.css('h1').text.strip)
100
- end
81
+ private
101
82
 
102
- def scrape_brewery
103
- brewery_element = doc.at_css("a[itemprop='brand']")
104
- brewery_id = id_from_link(brewery_element)
105
- brewery_name = fix_characters(brewery_element.text)
106
- @brewery = Brewery.new(brewery_id, name: brewery_name)
107
- end
83
+ def validate_beer
84
+ error_indicator = 'we didn\'t find this beer'
85
+ error_message = "Beer not found - #{id}"
86
+ raise PageNotFoundError, error_message if name == error_indicator
87
+ end
108
88
 
109
- def scrape_style
110
- style_element = doc.at_css("a[href^='/beerstyles']")
111
- style_id = id_from_link(style_element)
112
- style_name = fix_characters(style_element.text)
113
- @style = Style.new(style_id, name: style_name)
114
- end
89
+ def scrape_name
90
+ @name ||= fix_characters(doc.css('h1').text.strip)
91
+ end
115
92
 
116
- def scrape_glassware
117
- glassware_elements = doc.css("a[href^='/ShowGlassware.asp']")
118
- @glassware = glassware_elements.map do |el|
119
- [:id, :name].zip([el['href'].split('GWID=').last.to_i, el.text]).to_h
93
+ def scrape_brewery
94
+ brewery_element = doc.at_css("a[itemprop='brand']")
95
+ brewery_id = id_from_link(brewery_element)
96
+ brewery_name = fix_characters(brewery_element.text)
97
+ @brewery = Brewery::Brewery.new(brewery_id, name: brewery_name)
120
98
  end
121
- end
122
99
 
123
- def scrape_availability
124
- raw_info = info_root.css('td')[1]
125
- .css('table')
126
- .css('td')
127
- .children
128
- .children
129
- .map(&:text)
130
- .reject(&:empty?)
131
- .each_slice(2)
132
- .to_a
133
- .tap { |a| a.last.unshift('distribution') }
134
- .map do |(k, v)|
135
- [k =~ /bottl/ ? :bottling : symbolize_text(k), v]
136
- end
137
- @availability = raw_info.to_h.merge(seasonal: scrape_misc[:seasonal])
138
- end
100
+ def scrape_style
101
+ style_element = doc.at_css("a[href^='/beerstyles']")
102
+ style_id = id_from_link(style_element)
103
+ style_name = fix_characters(style_element.text)
104
+ @style = Style.new(style_id, name: style_name)
105
+ end
139
106
 
140
- def scrape_abv
141
- @abv = scrape_misc[:abv]
142
- end
107
+ def scrape_glassware
108
+ glassware_elements = doc.css("a[href^='/ShowGlassware.asp']")
109
+ @glassware = glassware_elements.map do |el|
110
+ [:id, :name].zip([el['href'].split('GWID=').last.to_i, el.text]).to_h
111
+ end
112
+ end
143
113
 
144
- def scrape_calories
145
- @calories = scrape_misc[:est_calories]
146
- end
114
+ def scrape_abv
115
+ @abv = scrape_misc[:abv]
116
+ end
147
117
 
148
- def scrape_description
149
- @description = info_root.next_element
150
- .next_element
151
- .children
152
- .children
153
- .map(&:text)
154
- .map(&:strip)
155
- .drop(1)
156
- .reject(&:empty?)
157
- .join("\n")
158
- @description = fix_characters(@description)
159
- end
118
+ def scrape_description
119
+ @description = fix_characters(doc.at_css('#_description3').text)
120
+ end
160
121
 
161
- def scrape_retired
162
- @retired = !(root.css('span.beertitle2') &&
163
- root.css('span.beertitle2').text =~ /RETIRED/).nil?
164
- end
122
+ def scrape_retired
123
+ element = doc.at_css('span.beertitle2')
124
+ @retired = element && element.text.match?(/RETIRED/) || false
125
+ end
165
126
 
166
- def scrape_rating
167
- raw_rating = [:overall,
168
- :style].zip(info_root.css('div')
169
- .select { |d| d['title'] =~ /This figure/ }
170
- .map { |d| d['title'].split(':').first.to_f }).to_h
171
- @rating = raw_rating.merge(ratings: scrape_misc[:ratings],
172
- weighted_avg: scrape_misc[:weighted_avg],
173
- mean: scrape_misc[:mean])
174
- end
127
+ def scrape_rating
128
+ raw_rating = [:overall,
129
+ :style].zip(doc.css('#_aggregateRating6 div')
130
+ .select { |d| d['title'] =~ /This figure/ }
131
+ .map { |d| d['title'].split(':').first.to_f }).to_h
132
+ @rating = raw_rating.merge(ratings: scrape_misc[:ratings],
133
+ weighted_avg: scrape_misc[:weighted_avg],
134
+ mean: scrape_misc[:mean])
135
+ end
175
136
 
176
- # Scrapes the miscellaneous information contained on the beer page.
177
- #
178
- # This information relates to various other specific types of information.
179
- # As such, other scrapers rely on this method for information.
180
- def scrape_misc
181
- info_root.next_element
182
- .first_element_child
183
- .children
184
- .map(&:text)
185
- .flat_map { |x| x.gsub(nbsp, ' ').strip.split(':') }
186
- .map(&:strip)
187
- .reject(&:empty?)
188
- .each_slice(2)
189
- .map { |(k, v)| [symbolize_text(k), v.to_f.zero? ? v : v.to_f] }
190
- .to_h
137
+ # Scrapes the miscellaneous information contained on the beer page.
138
+ #
139
+ # This information relates to various other specific types of information.
140
+ # As such, other scrapers rely on this method for information.
141
+ def scrape_misc
142
+ doc.at_css('.stats-container')
143
+ .children
144
+ .map(&:text)
145
+ .flat_map { |x| x.gsub(nbsp, ' ').strip.split(':') }
146
+ .map(&:strip)
147
+ .reject(&:empty?)
148
+ .each_slice(2)
149
+ .map { |(k, v)| [symbolize_text(k), v.to_f.zero? ? v : v.to_f] }
150
+ .to_h
151
+ end
191
152
  end
192
153
  end
193
154
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../urls'
4
+
5
+ module RateBeer
6
+ module Beer
7
+ # Redirects a beer if it is an alias.
8
+ #
9
+ def redirect_if_alias(doc)
10
+ Alias.new(doc).try_redirect
11
+ end
12
+
13
+ # The Alias class.
14
+ #
15
+ # RateBeer treats certain beers as aliases of another (e.g. Koenig Ludwig
16
+ # Weissbier - https://www.ratebeer.com/beer/konig-ludwig-weissbier/14945/)
17
+ # and provides a link to the "original" beer. This class is used to handle
18
+ # redirection where a beer is an alias.
19
+ #
20
+ class Alias
21
+ include RateBeer::URLs
22
+
23
+ # CSS selector for container with alias information.
24
+ ALIAS_SELECTOR = '.row.columns-container .col-sm-8'.freeze
25
+
26
+ # Create an Alias instance.
27
+ #
28
+ # The Alias class deals with handling beers which may be aliases of
29
+ # others, and so requires redirection to the "proper" beer's page.
30
+ #
31
+ # @param [Nokogiri::Doc] document representing a RateBeer.com beer page
32
+ #
33
+ def initialize(doc)
34
+ @doc = doc
35
+ end
36
+
37
+ # Redirects this beer to the "proper" beer page if it represents an alias
38
+ # of another beer.
39
+ #
40
+ # This method returns a new doc value if the beer is an alias, or nil if
41
+ # not.
42
+ def try_redirect
43
+ redirect_to_alias if aliased_beer?
44
+ end
45
+
46
+ private
47
+
48
+ def aliased_beer?
49
+ alias_pattern = /Also known as(.|\n)*Proceed to the aliased beer\.{3}/
50
+ alias_container && alias_container.text =~ alias_pattern
51
+ end
52
+
53
+ def redirect_to_alias
54
+ alias_node = alias_container.at_css('a')
55
+ alias_id = alias_node['href'].split('/').last.to_i
56
+ noko_doc(URI.join(BASE_URL, beer_url(alias_id)))
57
+ end
58
+
59
+ def alias_container
60
+ @doc.at_css(ALIAS_SELECTOR)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -2,187 +2,114 @@
2
2
 
3
3
  require_relative 'scraping'
4
4
  require_relative 'urls'
5
+ require_relative 'brewery/beer_list'
5
6
 
6
7
  module RateBeer
7
8
  # The brewery class represents one brewery found in RateBeer, with methods
8
9
  # for accessing information found about the brewery on the site.
9
- class Brewery
10
- # Each key represents an item of data accessible for each beer, and defines
11
- # dynamically a series of methods for accessing this data.
12
- #
13
- def self.data_keys
14
- [:name,
15
- :type,
16
- :address,
17
- :telephone,
18
- :beers]
19
- end
20
-
21
- include RateBeer::Scraping
22
- include RateBeer::URLs
23
-
24
- attr_reader :established, :location
25
-
26
- # CSS selector for the brewery information element.
27
- INFO_SELECTOR = "div[itemtype='http://schema.org/LocalBusiness']".freeze
28
-
29
- # Create RateBeer::Brewery instance.
30
- #
31
- # Requires the RateBeer ID# for the brewery in question. Optionally accepts
32
- # a name parameter where the name is already known.
33
- #
34
- # @param [Integer, String] id ID# for the brewery
35
- # @param [String] name The name of the specified brewery
36
- # @param [hash] options Options hash for entity created
37
- #
38
- def initialize(id, name: nil, **options)
39
- super
40
- if options
41
- @established = options[:established]
42
- @location = options[:location]
43
- @type = options[:type]
44
- @status = options[:status]
10
+ module Brewery
11
+ class Brewery
12
+ # Each key represents an item of data accessible for each beer, and defines
13
+ # dynamically a series of methods for accessing this data.
14
+ #
15
+ def self.data_keys
16
+ [:name,
17
+ :type,
18
+ :address,
19
+ :telephone,
20
+ :beers]
45
21
  end
46
- end
47
-
48
- def doc
49
- @doc ||= noko_doc(URI.join(BASE_URL, brewery_url(id)))
50
- validate_brewery
51
- @doc
52
- end
53
-
54
- def info_root
55
- @info_root ||= doc.at_css(INFO_SELECTOR)
56
- end
57
22
 
58
- private
59
-
60
- # Validates whether the brewery with the given ID exists.
61
- #
62
- # Throws an exception if the brewery does not exist.
63
- def validate_brewery
64
- error_message = "This brewer, ID##{id}, is no longer in the database. "\
65
- 'RateBeer Home'
66
- if @doc.at_css('body p').text == error_message
67
- raise PageNotFoundError.new("Brewery not found - #{id}")
23
+ include RateBeer::Scraping
24
+ include RateBeer::URLs
25
+
26
+ attr_reader :established, :location
27
+
28
+ # CSS selector for the brewery information element.
29
+ INFO_SELECTOR = "div[itemtype='http://schema.org/LocalBusiness']".freeze
30
+
31
+ # Create RateBeer::Brewery instance.
32
+ #
33
+ # Requires the RateBeer ID# for the brewery in question. Optionally accepts
34
+ # a name parameter where the name is already known.
35
+ #
36
+ # @param [Integer, String] id ID# for the brewery
37
+ # @param [String] name The name of the specified brewery
38
+ # @param [hash] options Options hash for entity created
39
+ #
40
+ def initialize(id, name: nil, **options)
41
+ super
42
+ if options
43
+ @established = options[:established]
44
+ @location = options[:location]
45
+ @type = options[:type]
46
+ @status = options[:status]
47
+ end
68
48
  end
69
- end
70
-
71
- # Scrapes the brewery's name.
72
- def scrape_name
73
- @name = fix_characters(info_root.css('h1').first.text)
74
- end
75
49
 
76
- # Scrapes the brewery's address.
77
- def scrape_address
78
- address_root = info_root.css('div[itemprop="address"] b span')
79
- address_details = address_root.map { |e| extract_address_element(e) }
80
- @address = address_details.to_h
81
- end
82
-
83
- # Extracts one element of address details from a node contained within the
84
- # address div.
85
- def extract_address_element(node)
86
- key = case node.attributes['itemprop'].value
87
- when 'streetAddress' then :street
88
- when 'addressLocality' then :city
89
- when 'addressRegion' then :state
90
- when 'addressCountry' then :country
91
- when 'postalCode' then :postcode
92
- else raise 'unrecognised attribute'
93
- end
94
- [key, node.text.strip]
95
- end
50
+ def doc
51
+ @doc ||= noko_doc(URI.join(BASE_URL, brewery_url(id)))
52
+ validate_brewery
53
+ @doc
54
+ end
96
55
 
97
- # Scrapes the telephone number of the brewery.
98
- def scrape_telephone
99
- @telephone = info_root.at_css('span[itemprop="telephone"]')
100
- end
56
+ def info_root
57
+ @info_root ||= doc.at_css(INFO_SELECTOR)
58
+ end
101
59
 
102
- # Scrapes the type of brewery.
103
- def scrape_type
104
- @type = info_root.css('div')[1]
105
- end
60
+ private
61
+
62
+ # Validates whether the brewery with the given ID exists.
63
+ #
64
+ # Throws an exception if the brewery does not exist.
65
+ def validate_brewery
66
+ error_message = "This brewer, ID##{id}, is no longer in the database. "\
67
+ 'RateBeer Home'
68
+ if @doc.at_css('body p').text == error_message
69
+ raise PageNotFoundError.new("Brewery not found - #{id}")
70
+ end
71
+ end
106
72
 
107
- # Scrapes beers list for brewery.
108
- def scrape_beers
109
- beers_doc = noko_doc(URI.join(BASE_URL, brewery_beers_url(id)))
110
- rows = beers_doc.css('table#brewer-beer-table tbody tr')
111
- @beers = rows.map { |row| process_beer_row(row) }.reject(&:nil?)
112
- end
73
+ # Scrapes the brewery's name.
74
+ def scrape_name
75
+ @name = fix_characters(info_root.css('h1').first.text)
76
+ end
113
77
 
114
- # Process a row of data representing one beer brewed by/at a brewery.
115
- #
116
- # @param [Nokogiri::XML::Element] row HTML TR row wrapped as a Nokogiri
117
- # element
118
- # @param [String] location the location at which a brewery's beer is brewed
119
- # where this location differs from the brewery's regular brewsite/venue
120
- # @param [String] brewer the client for whom this brewery brewed the beer,
121
- # where the brewery is brewing for a different company/brewery
122
- # @return [RateBeer::Beer] a beer object representing the scraped beer,
123
- # containing scraped attributes
124
- #
125
- def process_beer_row(row)
126
- beer = process_beer_name_cell(row.css('td').first)
127
- beer[:abv] = row.css('td')[1].text.to_f
128
- beer[:date_added] = Date.strptime(row.css('td')[2].text, '%m/%d/%Y')
129
- Beer.new(id, beer.merge(process_rating_info(row)))
130
- end
78
+ # Scrapes the brewery's address.
79
+ def scrape_address
80
+ address_root = info_root.css('div[itemprop="address"] b span')
81
+ address_details = address_root.map { |e| extract_address_element(e) }
82
+ @address = address_details.to_h
83
+ end
131
84
 
132
- # Processes the cell containing the beer's name and other information.
133
- #
134
- # This cell contains information on the beer's name, its style, whether it
135
- # is retired, and who is was brewed for or by.
136
- def process_beer_name_cell(node)
137
- beer_link = node.at_css('strong a')
138
- name = fix_characters(beer_link.text)
139
- id = id_from_link(beer_link)
140
- info = node.at_css('em.real-small')
141
- brewed_at_for = process_brewed_at_for(node)
142
- style = process_style_info(node)
143
- { id: id,
144
- name: name,
145
- style: style,
146
- retired: info && info.text =~ /retired/ || false }.merge(brewed_at_for)
147
- end
85
+ # Extracts one element of address details from a node contained within the
86
+ # address div.
87
+ def extract_address_element(node)
88
+ key = case node.attributes['itemprop'].value
89
+ when 'streetAddress' then :street
90
+ when 'addressLocality' then :city
91
+ when 'addressRegion' then :state
92
+ when 'addressCountry' then :country
93
+ when 'postalCode' then :postcode
94
+ else raise 'unrecognised attribute'
95
+ end
96
+ [key, node.text.strip]
97
+ end
148
98
 
149
- # Processes information on who the beer was brewed for or by, or at.
150
- def process_brewed_at_for(node)
151
- brewed_at_for_node = node.at_css('div.small em')
152
- return {} if brewed_at_for_node.nil?
153
- node_text = brewed_at_for_node.children.first.text
154
- key = if node_text.include?('Brewed at')
155
- :brewed_at
156
- elsif node_text.include?('Brewed by/for')
157
- :brewed_by_for
158
- end
159
- other_brewer_node = brewed_at_for_node.at_css('a')
160
- { key => Brewery.new(id_from_link(other_brewer_node),
161
- name: other_brewer_node.text) }
162
- end
99
+ # Scrapes the telephone number of the brewery.
100
+ def scrape_telephone
101
+ @telephone = info_root.at_css('span[itemprop="telephone"]')
102
+ end
163
103
 
164
- # Processes the style information contained within a beer name cell.
165
- def process_style_info(node)
166
- style_node = node.css('a').find do |n|
167
- n.children.any? { |c| c.name == 'span' }
104
+ # Scrapes the type of brewery.
105
+ def scrape_type
106
+ @type = info_root.css('div')[1]
168
107
  end
169
- name = style_node.text
170
- id = id_from_link(style_node)
171
- Style.new(id, name: name)
172
- end
173
108
 
174
- # Processes rating information from a beer row.
175
- def process_rating_info(row)
176
- cell_indices = { avg_rating: 4,
177
- overall_rating: 5,
178
- style_rating: 6,
179
- num_ratings: 7 }
180
- rating = cell_indices.map do |attr, i|
181
- val = row.css('td')[i].text.gsub(nbsp, ' ').strip
182
- conversion = attr == :avg_rating ? :to_f : :to_i
183
- [attr, val.send(conversion)]
109
+ # Scrapes beers list for brewery.
110
+ def scrape_beers
111
+ @beers = BeerList.new(self).beers
184
112
  end
185
- rating.to_h
186
113
  end
187
114
  end
188
115
  end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../urls'
4
+
5
+ module RateBeer
6
+ module Brewery
7
+ # The BeerList class extracts a list of beers for a given brewery.
8
+ class BeerList
9
+ include RateBeer::Scraping
10
+ include RateBeer::URLs
11
+
12
+ attr_reader :brewery, :id
13
+
14
+ def initialize(brewery)
15
+ @brewery = brewery
16
+ @id = brewery.id
17
+ end
18
+
19
+ def beers
20
+ beers_doc = noko_doc(URI.join(BASE_URL, brewery_beers_url(id)))
21
+ rows = beers_doc.css('table#brewer-beer-table tbody tr')
22
+ @beers = rows.map { |row| process_beer_row(row) }.reject(&:nil?)
23
+ end
24
+
25
+ private
26
+
27
+ # Process a row of data representing one beer brewed by/at a brewery.
28
+ #
29
+ # @param [Nokogiri::XML::Element] row HTML TR row wrapped as a Nokogiri
30
+ # element
31
+ # @param [String] location the location at which a brewery's beer is brewed
32
+ # where this location differs from the brewery's regular brewsite/venue
33
+ # @param [String] brewer the client for whom this brewery brewed the beer,
34
+ # where the brewery is brewing for a different company/brewery
35
+ # @return [RateBeer::Beer] a beer object representing the scraped beer,
36
+ # containing scraped attributes
37
+ #
38
+ def process_beer_row(row)
39
+ beer = process_beer_name_cell(row.css('td').first)
40
+ beer[:abv] = row.css('td')[1].text.to_f
41
+ beer[:date_added] = Date.strptime(row.css('td')[2].text, '%m/%d/%Y')
42
+ Beer::Beer.new(id, beer.merge(process_rating_info(row)))
43
+ end
44
+
45
+ # Processes the cell containing the beer's name and other information.
46
+ #
47
+ # This cell contains information on the beer's name, its style, whether it
48
+ # is retired, and who is was brewed for or by.
49
+ def process_beer_name_cell(node)
50
+ beer_link = node.at_css('strong a')
51
+ name = fix_characters(beer_link.text)
52
+ id = id_from_link(beer_link)
53
+ info = node.at_css('em.real-small')
54
+ brewed_at_for = process_brewed_at_for(node)
55
+ style = process_style_info(node)
56
+ { id: id,
57
+ name: name,
58
+ style: style,
59
+ retired: info && info.text =~ /retired/ || false }.merge(brewed_at_for)
60
+ end
61
+
62
+ # Processes information on who the beer was brewed for or by, or at.
63
+ def process_brewed_at_for(node)
64
+ brewed_at_for_node = node.at_css('div.small em')
65
+ return {} if brewed_at_for_node.nil?
66
+ node_text = brewed_at_for_node.children.first.text
67
+ key = if node_text.include?('Brewed at')
68
+ :brewed_at
69
+ elsif node_text.include?('Brewed by/for')
70
+ :brewed_by_for
71
+ end
72
+ other_brewer_node = brewed_at_for_node.at_css('a')
73
+ { key => Brewery.new(id_from_link(other_brewer_node),
74
+ name: other_brewer_node.text) }
75
+ end
76
+
77
+ # Processes the style information contained within a beer name cell.
78
+ def process_style_info(node)
79
+ style_node = node.css('a').find do |n|
80
+ n.children.any? { |c| c.name == 'span' }
81
+ end
82
+ name = style_node.text
83
+ id = id_from_link(style_node)
84
+ Style.new(id, name: name)
85
+ end
86
+
87
+ # Processes rating information from a beer row.
88
+ def process_rating_info(row)
89
+ cell_indices = { avg_rating: 4,
90
+ style_rating: 5,
91
+ num_ratings: 6 }
92
+ rating = cell_indices.map do |attr, i|
93
+ val = row.css('td')[i].text.gsub(nbsp, ' ').strip
94
+ conversion = attr == :avg_rating ? :to_f : :to_i
95
+ [attr, val.send(conversion)]
96
+ end
97
+ rating.to_h
98
+ end
99
+ end
100
+ end
101
+ end
@@ -40,10 +40,6 @@ module RateBeer
40
40
  @doc
41
41
  end
42
42
 
43
- def heading
44
- @heading ||= doc.at_css('.col-lg-9')
45
- end
46
-
47
43
  private
48
44
 
49
45
  def validate_location
@@ -53,20 +49,19 @@ module RateBeer
53
49
  end
54
50
 
55
51
  def scrape_name
56
- @name = heading.at_css('h1')
57
- .text
58
- .split('Breweries')
59
- .first
60
- .strip
52
+ @name = doc.at_css('h1')
53
+ .text
54
+ .split('Breweries')
55
+ .first
56
+ .strip
61
57
  end
62
58
 
63
59
  def scrape_num_breweries
64
- @num_breweries = heading.at_css('li.active')
65
- .text
66
- .scan(/Active \((\d*)\)/)
67
- .first
68
- .first
69
- .to_i
60
+ @num_breweries = doc.at_css('li.active')
61
+ .text
62
+ .split(' ')
63
+ .first
64
+ .to_i
70
65
  end
71
66
 
72
67
  def scrape_breweries
@@ -86,12 +81,12 @@ module RateBeer
86
81
  .strip
87
82
  type = cells[1].text.strip
88
83
  established = status == 'Active' ? cells[3].text.to_i : nil
89
- Brewery.new(id,
90
- name: name,
91
- location: location,
92
- type: type,
93
- established: established,
94
- status: status)
84
+ Brewery::Brewery.new(id,
85
+ name: name,
86
+ location: location,
87
+ type: type,
88
+ established: established,
89
+ status: status)
95
90
  end
96
91
  end
97
92
  end
@@ -57,11 +57,11 @@ module RateBeer
57
57
  # beer, up to the review_limit
58
58
  #
59
59
  def retrieve(beer, order: :most_recent, limit: 10)
60
- if beer.is_a?(RateBeer::Beer)
60
+ if beer.is_a?(RateBeer::Beer::Beer)
61
61
  beer_id = beer.id
62
62
  elsif beer.is_a?(Integer)
63
63
  beer_id = beer
64
- beer = RateBeer::Beer.new(beer)
64
+ beer = RateBeer::Beer::Beer.new(beer)
65
65
  else
66
66
  raise "unknown beer value: #{beer}"
67
67
  end
@@ -69,7 +69,7 @@ module RateBeer
69
69
  reviews = num_pages(limit).times.flat_map do |page_number|
70
70
  url = URI.join(BASE_URL, review_url(beer_id, url_suffix(order), page_number))
71
71
  doc = RateBeer::Scraping.noko_doc(url)
72
- root = doc.css("#container table table")[3]
72
+ root = doc.at_css('.reviews-container')
73
73
 
74
74
  # All reviews are contained within the sole cell in the sole row of
75
75
  # the selected table. Each review consists of rating information,
@@ -77,7 +77,7 @@ module RateBeer
77
77
  #
78
78
  # The components are contained within div, small, div tags
79
79
  # respectively. We need to scrape these specifically.
80
- root.css('td')
80
+ root.at_css('div div')
81
81
  .children
82
82
  .select { |x| x.name == 'div' || x.name == 'small' }
83
83
  .map(&:text)
@@ -126,7 +126,7 @@ module RateBeer
126
126
  @beer = if options[:beer].is_a?(RateBeer::Beer)
127
127
  options[:beer]
128
128
  elsif options[:beer].is_a?(Integer)
129
- RateBeer::Beer.new(options[:beer])
129
+ RateBeer::Beer::Beer.new(options[:beer])
130
130
  else
131
131
  raise ArgumentError.new("incorrect beer parameter: #{options[:beer]}")
132
132
  end
@@ -14,10 +14,12 @@ module RateBeer
14
14
 
15
15
  # Run method on inclusion in class.
16
16
  def self.included(base)
17
- base.data_keys.each do |attr|
18
- define_method(attr) do
19
- send("scrape_#{attr}") unless instance_variable_defined?("@#{attr}")
20
- instance_variable_get("@#{attr}")
17
+ if base.respond_to?(:data_keys)
18
+ base.data_keys.each do |attr|
19
+ define_method(attr) do
20
+ send("scrape_#{attr}") unless instance_variable_defined?("@#{attr}")
21
+ instance_variable_get("@#{attr}")
22
+ end
21
23
  end
22
24
  end
23
25
  end
@@ -128,7 +130,7 @@ module RateBeer
128
130
  # strings scraped from RateBeer.com
129
131
  #
130
132
  def fix_characters(string)
131
- string = string.encode('UTF-8', 'binary', invalid: :replace, undef: :replace, replace: '')
133
+ string = string.encode('UTF-8', invalid: :replace, undef: :replace, replace: '')
132
134
  characters = { nbsp => " ",
133
135
  "\u0093" => "ž",
134
136
  "\u0092" => "'",
@@ -108,7 +108,7 @@ module RateBeer
108
108
  def scrape_beers
109
109
  unless instance_variable_defined?('@beers')
110
110
  run_search
111
- @beers = @beers.sort_by(&:id)
111
+ @beers = @beers && @beers.sort_by(&:id)
112
112
  end
113
113
  @beers
114
114
  end
@@ -124,7 +124,7 @@ module RateBeer
124
124
  # Generate parameters to use in POST request.
125
125
  #
126
126
  def post_params
127
- { 'BeerName' => @query }
127
+ { 'beername' => @query }
128
128
  end
129
129
 
130
130
  # Process breweries table returned in search.
@@ -146,7 +146,7 @@ module RateBeer
146
146
  end
147
147
  result[:url] = row.at_css('a')['href']
148
148
  result[:id] = result[:url].split('/').last.to_i
149
- Brewery.new(result[:id], name: result[:name])
149
+ Brewery::Brewery.new(result[:id], name: result[:name])
150
150
  end
151
151
  end
152
152
 
@@ -183,14 +183,14 @@ module RateBeer
183
183
  def process_beer_row(row)
184
184
  result = [:id, :name, :score, :ratings, :url].zip([nil]).to_h
185
185
  content = row.element_children.map { |x| fix_characters(x.text) }
186
- result[:name] = content.first
186
+ result[:name] = row.element_children.first.at_css('a').text
187
187
  result[:score], result[:ratings] = content.values_at(3, 4)
188
188
  .map do |n|
189
189
  n.nil? || n.empty? ? nil : n.to_i
190
190
  end
191
191
  result[:url] = row.at_css('a')['href']
192
192
  result[:id] = result[:url].split('/').last.to_i
193
- b = Beer.new(result[:id], name: result[:name])
193
+ b = Beer::Beer.new(result[:id], name: result[:name])
194
194
  b.brewery.name if @scrape_beer_brewers
195
195
  b
196
196
  end
@@ -35,9 +35,9 @@ module RateBeer
35
35
  #
36
36
  def all_styles(include_hidden = false)
37
37
  doc = Scraping.noko_doc(URI.join(BASE_URL, '/beerstyles/'))
38
- root = doc.at_css('div.container-fluid table')
38
+ root = doc.at_css('div.container-fluid')
39
39
 
40
- categories = root.css('.groupname').map(&:text)
40
+ categories = root.css('h3').map(&:text)
41
41
  style_node = root.css('.styleGroup')
42
42
 
43
43
  styles = style_node.flat_map.with_index do |list, i|
@@ -109,8 +109,8 @@ module RateBeer
109
109
  @beers = beer_list.css('tr').drop(1).map do |row|
110
110
  cells = row.css('td')
111
111
  url = cells[1].at_css('a')['href']
112
- [cells[0].text.to_i, Beer.new(url.split('/').last,
113
- name: fix_characters(cells[1].text))]
112
+ [cells[0].text.to_i, Beer::Beer.new(url.split('/').last,
113
+ name: fix_characters(cells[1].text))]
114
114
  end.to_h
115
115
  end
116
116
  end
data/lib/ratebeer/urls.rb CHANGED
@@ -1,10 +1,11 @@
1
- module RateBeer
1
+ # frozen_string_literal: true
2
2
 
3
+ module RateBeer
3
4
  # This module contains URLs or URL patterns for use throughout the Gem.
4
5
  #
5
6
  module URLs
6
- BASE_URL = "http://www.ratebeer.com"
7
- SEARCH_URL = "/findbeer.asp"
7
+ BASE_URL = 'https://www.ratebeer.com'
8
+ SEARCH_URL = '/search'
8
9
 
9
10
  # Return URL to info page for beer with id
10
11
  #
@@ -1,23 +1,23 @@
1
1
  require 'spec_helper'
2
2
 
3
- describe RateBeer::Beer do
3
+ describe RateBeer::Beer::Beer do
4
4
  before :all do
5
- @valid = RateBeer::Beer.new(1411) # ID for Tennents Lager (sorry...)
6
- @retired = RateBeer::Beer.new(213_225) # ID for BrewDog Vice Bier
7
- @with_name = RateBeer::Beer.new(422, name: 'Stone IPA')
5
+ @valid = RateBeer.beer(1411) # ID for Tennents Lager (sorry...)
6
+ @retired = RateBeer.beer(213_225) # ID for BrewDog Vice Bier
7
+ @with_name = RateBeer.beer(422, 'Stone IPA')
8
8
  end
9
9
 
10
10
  describe '#new' do
11
11
  it 'creates a beer instance' do
12
- expect(@valid).to be_a RateBeer::Beer
12
+ expect(@valid).to be_a RateBeer::Beer::Beer
13
13
  end
14
14
 
15
15
  it 'requires an ID# as parameter' do
16
- expect { RateBeer::Beer.new }.to raise_error(ArgumentError)
16
+ expect { RateBeer.beer }.to raise_error(ArgumentError)
17
17
  end
18
18
 
19
19
  it 'accepts a name parameter' do
20
- expect(@with_name).to be_a RateBeer::Beer
20
+ expect(@with_name).to be_a RateBeer::Beer::Beer
21
21
  end
22
22
  end
23
23
 
@@ -46,9 +46,7 @@ describe RateBeer::Beer do
46
46
  :url,
47
47
  :style,
48
48
  :glassware,
49
- :availability,
50
49
  :abv,
51
- :calories,
52
50
  :description,
53
51
  :retired,
54
52
  :rating)
@@ -2,21 +2,21 @@ require 'spec_helper'
2
2
 
3
3
  describe RateBeer::Brewery do
4
4
  before :all do
5
- @valid = RateBeer::Brewery.new(8534) # ID for BrewDog
6
- @with_name = RateBeer::Brewery.new(1069, name: 'Cantillon Brewery')
5
+ @valid = RateBeer.brewery(8534) # ID for BrewDog
6
+ @with_name = RateBeer.brewery(1069, 'Cantillon Brewery')
7
7
  end
8
8
 
9
9
  describe '#new' do
10
10
  it 'creates a brewery instance' do
11
- expect(@valid).to be_a RateBeer::Brewery
11
+ expect(@valid).to be_a RateBeer::Brewery::Brewery
12
12
  end
13
13
 
14
14
  it 'requires an ID# as parameter' do
15
- expect { RateBeer::Brewery.new }.to raise_error(ArgumentError)
15
+ expect { RateBeer::Brewery::Brewery.new }.to raise_error(ArgumentError)
16
16
  end
17
17
 
18
18
  it 'accepts a name parameter' do
19
- expect(@with_name).to be_a RateBeer::Brewery
19
+ expect(@with_name).to be_a RateBeer::Brewery::Brewery
20
20
  end
21
21
  end
22
22
 
@@ -36,7 +36,7 @@ describe RateBeer::Brewery do
36
36
  end
37
37
 
38
38
  it 'returns an array of beer instances' do
39
- @valid.beers.each { |b| expect(b).to be_a RateBeer::Beer }
39
+ @valid.beers.each { |b| expect(b).to be_a RateBeer::Beer::Beer }
40
40
  end
41
41
 
42
42
  it 'returns a list of beers produced by brewery' do
@@ -48,7 +48,7 @@ describe RateBeer::Brewery do
48
48
  162_521,
49
49
  87_321,
50
50
  118_987,
51
- 119_594].map { |id| RateBeer::Beer.new(id) }
51
+ 119_594].map { |id| RateBeer.beer(id) }
52
52
  expect(@valid.beers).to include(*beers)
53
53
  end
54
54
  end
@@ -41,7 +41,7 @@ describe RateBeer::Country do
41
41
  2809,
42
42
  2878,
43
43
  11582,
44
- 8031].map { |i| RateBeer::Brewery.new(i) }
44
+ 8031].map { |i| RateBeer::Brewery::Brewery.new(i) }
45
45
  expect(@country.breweries).to include(*expected_breweries)
46
46
  end
47
47
 
@@ -39,7 +39,7 @@ describe RateBeer::Region do
39
39
  it "returns a series of RateBeer::Brewery instances" do
40
40
  expected_breweries = [25_211,
41
41
  3132,
42
- 1097].map { |i| RateBeer::Brewery.new(i) }
42
+ 1097].map { |i| RateBeer::Brewery::Brewery.new(i) }
43
43
  expect(@region.breweries).to include(*expected_breweries)
44
44
  end
45
45
 
@@ -3,7 +3,7 @@ require 'spec_helper'
3
3
  describe RateBeer::Review do
4
4
  before :all do
5
5
  # Create Review instance with Beer instance
6
- @beer = RateBeer::Beer.new(135361) # BrewDog Punk IPA
6
+ @beer = RateBeer.beer(135361) # BrewDog Punk IPA
7
7
  @constructed_params = { beer: @beer,
8
8
  reviewer: "Johnny Tester",
9
9
  reviewer_rank: 1234,
@@ -43,7 +43,7 @@ describe RateBeer::Review do
43
43
  end
44
44
 
45
45
  it "recognises a beer specified by ID#" do
46
- expect(@reviews_by_id.first.beer).to eq RateBeer::Beer.new(@beer_id)
46
+ expect(@reviews_by_id.first.beer).to eq RateBeer.beer(@beer_id)
47
47
  end
48
48
 
49
49
  it "retrieves reviews for beer specified by ID#" do
@@ -18,7 +18,7 @@ describe RateBeer::Search do
18
18
  describe '#run_search' do
19
19
  before :all do
20
20
  @failed_search = RateBeer::Search.new('random param 1234').run_search
21
- @successful_search = RateBeer::Search.new('heineken').run_search
21
+ @successful_search = RateBeer::Search.new('dugges').run_search
22
22
  end
23
23
 
24
24
  it 'executes a search using specified query parameter' do
@@ -45,22 +45,15 @@ describe RateBeer::Search do
45
45
  it 'returns a list of specific beers matching the query parameter' do
46
46
  beers = @successful_search.beers
47
47
  names = beers.map(&:name)
48
- expect(names).to include('Heineken',
49
- 'Heineken Beer',
50
- 'Heineken Golden Fire Strong',
51
- 'Heineken Kylian',
52
- 'Heineken Oud Bruin',
53
- 'Heineken Tarwebok')
48
+ expect(names).to include('Dugges / Stillwater Tropic Thunder',
49
+ 'Dugges Almost Imperial',
50
+ 'Dugges Bärliner')
54
51
  end
55
52
 
56
53
  it 'returns a list of specific breweries matching the query parameter' do
57
54
  breweries = @successful_search.breweries
58
55
  names = breweries.map(&:name)
59
- expect(names).to include('Heineken UK',
60
- 'Heineken Italia',
61
- 'Al Ahram (Heineken)',
62
- 'Pivovar Corgon (Heineken)',
63
- 'Bralima (Heineken)')
56
+ expect(names).to include('Dugges Bryggeri')
64
57
  end
65
58
  end
66
59
  end
@@ -4,14 +4,14 @@ describe RateBeer do
4
4
  describe '.beer' do
5
5
  it 'creates a new beer' do
6
6
  beer = RateBeer.beer(1234, 'Magic Lager')
7
- expect(beer).to eq RateBeer::Beer.new(1234, name: 'Magic Lager')
7
+ expect(beer).to eq RateBeer::Beer::Beer.new(1234, name: 'Magic Lager')
8
8
  end
9
9
  end
10
10
 
11
11
  describe '.brewery' do
12
12
  it 'creates a new brewery' do
13
13
  brewery = RateBeer.brewery(456, 'Magic BrewCo')
14
- expect(brewery).to eq RateBeer::Brewery.new(456, name: 'Magic BrewCo')
14
+ expect(brewery).to eq RateBeer::Brewery::Brewery.new(456, name: 'Magic BrewCo')
15
15
  end
16
16
  end
17
17
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ratebeer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Dan Meakin
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2016-07-30 00:00:00.000000000 Z
11
+ date: 2017-02-19 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
@@ -38,7 +38,9 @@ dependencies:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
40
  version: '0'
41
- description: RateBeer provides a way to access information from RateBeer.com.
41
+ description: |-
42
+ RateBeer provides a way to access information from \
43
+ \RateBeer.com.
42
44
  email: dan@danmeakin.com
43
45
  executables: []
44
46
  extensions: []
@@ -52,7 +54,9 @@ files:
52
54
  - bin/ratebeer
53
55
  - lib/ratebeer.rb
54
56
  - lib/ratebeer/beer.rb
57
+ - lib/ratebeer/beer/alias.rb
55
58
  - lib/ratebeer/brewery.rb
59
+ - lib/ratebeer/brewery/beer_list.rb
56
60
  - lib/ratebeer/country.rb
57
61
  - lib/ratebeer/location.rb
58
62
  - lib/ratebeer/region.rb
@@ -89,7 +93,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
93
  version: '0'
90
94
  requirements: []
91
95
  rubyforge_project:
92
- rubygems_version: 2.5.1
96
+ rubygems_version: 2.6.8
93
97
  signing_key:
94
98
  specification_version: 4
95
99
  summary: Unofficial RateBeer API