uhaul 1.1.0 → 1.2.0

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
  SHA256:
3
- metadata.gz: 6abdb81d3f63d578fd92845c756fd0bc0d4673b53ded8071c20e9568bdb04067
4
- data.tar.gz: a0788d4f607f9774117d01e0e8590cdd01b46ce9f176dd8cb9b6feabd5a089b7
3
+ metadata.gz: 22343f801475f0781ff4ec3722470098b7d05e124b56253deb9d912d1dd54fac
4
+ data.tar.gz: f49c694569358b34e8dddf4a5493a5cbbce5ec7e77a19c2c2d3c8a7515f821d4
5
5
  SHA512:
6
- metadata.gz: 3b05f8006e9ddb4760c3c98dd2d40948e9df8d1a3752d6dead5a2a05844168c8d5be4a2aefa824f885d1a572dfbeaff725fd9eaa3828e49144691dd591f62293
7
- data.tar.gz: e8658751bc9003a8f734b0517d283c6c310c498fc10fab282b89c34b70291219b798f3a48843fa933e48d18c02a041bef62fe377d832862d240edbfc87a1e589
6
+ metadata.gz: 3ad0314baee02a85ca1587c923fd6748ad6c14833ee17536efe2a4251753ac8a1afd23d917585a93c31da4033dc4c37bd827e8ea5e9dfc483a534ad6b79b48ed
7
+ data.tar.gz: 81376338d60cd9f9875f9d9dfa3c72dd880fbd9821c3d73f678f0ed8d0dc7abc189ca596c2d479d8528e55f536fdf49b7fb1060b42c625a110269e59b94eb45e
data/lib/uhaul/address.rb CHANGED
@@ -3,8 +3,6 @@
3
3
  module UHaul
4
4
  # The address (street + city + state + zip) of a facility.
5
5
  class Address
6
- ADDRESS_SELECTOR = '.item-des-box .text-box .part_title_1'
7
- ADDRESS_REGEX = /(?<street>.+),\s+(?<city>.+),\s+(?<state>.+)\s+(?<zip>\d{5})/
8
6
  # @attribute [rw] street
9
7
  # @return [String]
10
8
  attr_accessor :street
@@ -48,16 +46,57 @@ module UHaul
48
46
  "#{street}, #{city}, #{state} #{zip}"
49
47
  end
50
48
 
49
+ # @param document [String]
50
+ # @param data [Hash] optional
51
+ #
52
+ # @return [Address]
53
+ def self.parse(document:, data:)
54
+ parse_by_data(data:) || parse_by_document(document:)
55
+ end
56
+
51
57
  # @param data [Hash]
52
58
  #
53
59
  # @return [Address]
54
- def self.parse(data:)
60
+ def self.parse_by_data(data:)
61
+ address = data&.dig('address')
62
+ return unless address
63
+
55
64
  new(
56
- street: data['streetAddress'],
57
- city: data['addressLocality'],
58
- state: data['addressRegion'],
59
- zip: data['postalCode']
65
+ street: address['streetAddress'],
66
+ city: address['addressLocality'],
67
+ state: address['addressRegion'],
68
+ zip: address['postalCode']
60
69
  )
61
70
  end
71
+
72
+ # @param document [Nokogiri::HTML::Document]
73
+ #
74
+ # @return [Address]
75
+ def self.parse_by_document(document:)
76
+ element = document.at_css('address')
77
+ return unless element
78
+
79
+ element.text.match(/(?<street>.+)[\r\n,]+(?<city>.+)[\r\n,]+(?<state>.+)[\r\n\s,]+(?<zip>\d{5})/) do |match|
80
+ new(
81
+ street: strip(match[:street]),
82
+ city: strip(match[:city]),
83
+ state: strip(match[:state]),
84
+ zip: strip(match[:zip])
85
+ )
86
+ end
87
+ end
88
+
89
+ # @param text [String]
90
+ #
91
+ # @return [String]
92
+ def self.strip(text)
93
+ return unless text
94
+
95
+ text
96
+ .strip
97
+ .gsub(/^[\s\p{Space},],+/, '')
98
+ .gsub(/[\s\p{Space},]+$/, '')
99
+ .gsub(/[\s\p{Space}]+/, ' ')
100
+ end
62
101
  end
63
102
  end
data/lib/uhaul/crawl.rb CHANGED
@@ -33,7 +33,7 @@ module UHaul
33
33
  @stdout.puts(facility.text)
34
34
  facility.prices.each { |price| @stdout.puts(price.text) }
35
35
  @stdout.puts
36
- rescue FetchError => e
36
+ rescue UHaul::Error => e
37
37
  @stderr.puts("url=#{url} error=#{e.message}")
38
38
  end
39
39
  end
@@ -3,8 +3,6 @@
3
3
  module UHaul
4
4
  # The dimensions (width + depth + sqft) of a price.
5
5
  class Dimensions
6
- class ParseError < StandardError; end
7
-
8
6
  DIMENSIONS_REGEX = /(?<width>[\d\.]+)'\s*x\s*(?<depth>[\d\.]+)'\s*x\s*(?<height>[\d\.]+)'/
9
7
 
10
8
  # @attribute [rw] depth
@@ -5,8 +5,6 @@ module UHaul
5
5
  #
6
6
  # e.g. https://www.uhaul.com/Locations/Self-Storage-near-Inglewood-CA-90301/712030/
7
7
  class Facility
8
- class ParseError < StandardError; end
9
-
10
8
  PRICES_SELECTOR = '#roomTypes > ul:not([id*="VehicleStorage"]) > li'
11
9
 
12
10
  SITEMAP_URLS = %w[
@@ -126,11 +124,11 @@ module UHaul
126
124
  def self.parse(url:, document:)
127
125
  data = parse_ld_json_script(document:)
128
126
 
129
- id = data['@id'].match(%r{(?<id>\d+)/#})[:id]
130
- name = data['name']
127
+ id = parse_id!(document:)
128
+ name = parse_name!(document:)
131
129
 
132
- geocode = Geocode.parse(data: data['geo'] || data['areaServed']['geoMidpoint'])
133
- address = Address.parse(data: data['address'])
130
+ geocode = Geocode.parse(data:, document:)
131
+ address = Address.parse(data:, document:)
134
132
  prices = document.css(PRICES_SELECTOR).map { |element| Price.parse(element:) }.compact
135
133
 
136
134
  new(id:, url:, name:, address:, geocode:, prices:)
@@ -144,7 +142,7 @@ module UHaul
144
142
  def self.parse_ld_json_script(document:)
145
143
  parse_ld_json_scripts(document:).find do |data|
146
144
  %w[SelfStorage LocalBusiness].include?(data['@type'])
147
- end || raise(ParseError, 'missing ld+json')
145
+ end
148
146
  end
149
147
 
150
148
  # @param document [Nokogiri::HTML::Document]
@@ -156,6 +154,29 @@ module UHaul
156
154
  elements.map { |element| element.text.empty? ? {} : JSON.parse(element.text) }
157
155
  end
158
156
 
157
+ # @param document [Nokogiri::HTML::Document]
158
+ #
159
+ # @raise [ParseError]
160
+ #
161
+ # @return [String]
162
+ def self.parse_id!(document:)
163
+ element = document.at_xpath('//link[@rel="canonical"]') || raise(ParseError, 'missing <link rel="canonical">')
164
+
165
+ href = element['href']
166
+ href.match(%r{(?<id>\d+)/$})[:id]
167
+ end
168
+
169
+ # @param document [Nokogiri::HTML::Document]
170
+ #
171
+ # @raise [ParseError]
172
+ #
173
+ # @return [String]
174
+ def self.parse_name!(document:)
175
+ element = document.at_xpath('//title') || raise(ParseError, 'missing <title>...</title>')
176
+
177
+ element.text.match(/\|\s*(?<name>.*)\s*$/)[:name].strip
178
+ end
179
+
159
180
  # @param id [String]
160
181
  # @param url [String]
161
182
  # @param name [String]
data/lib/uhaul/geocode.rb CHANGED
@@ -14,16 +14,6 @@ module UHaul
14
14
  # @return [Float]
15
15
  attr_accessor :longitude
16
16
 
17
- # @param data [Hash]
18
- #
19
- # @return [Geocode]
20
- def self.parse(data:)
21
- latitude = Float(data['latitude'])
22
- longitude = Float(data['longitude'])
23
-
24
- new(latitude:, longitude:)
25
- end
26
-
27
17
  # @param latitude [Float]
28
18
  # @param longitude [Float]
29
19
  def initialize(latitude:, longitude:)
@@ -44,5 +34,38 @@ module UHaul
44
34
  def text
45
35
  "#{@latitude},#{@longitude}"
46
36
  end
37
+
38
+ # @param document [String]
39
+ # @param data [Hash] optional
40
+ #
41
+ # @return [Address]
42
+ def self.parse(document:, data:)
43
+ parse_by_data(data:) || parse_by_document(document:)
44
+ end
45
+
46
+ # @param data [Hash]
47
+ #
48
+ # @return [Address]
49
+ def self.parse_by_data(data:)
50
+ coordinates = data&.dig('areaServed', 'geoMidpoint')
51
+ return unless coordinates
52
+
53
+ new(
54
+ latitude: Float(coordinates['latitude']),
55
+ longitude: Float(coordinates['longitude'])
56
+ )
57
+ end
58
+
59
+ # @param document [Nokogiri::HTML::Document]
60
+ #
61
+ # @return [Address]
62
+ def self.parse_by_document(document:)
63
+ document.text.match(/latitude:\s*(?<latitude>[\+\-\d\.]+),\s*longitude:\s*(?<longitude>[\+\-\d\.]+)/) do |match|
64
+ new(
65
+ latitude: Float(match[:latitude]),
66
+ longitude: Float(match[:longitude])
67
+ )
68
+ end
69
+ end
47
70
  end
48
71
  end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module UHaul
4
+ # Raised for unexpected HTTP responses.
5
+ class ParseError < Error
6
+ end
7
+ end
data/lib/uhaul/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module UHaul
4
- VERSION = '1.1.0'
4
+ VERSION = '1.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: uhaul
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-05-20 00:00:00.000000000 Z
11
+ date: 2025-05-21 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: http
@@ -105,6 +105,7 @@ files:
105
105
  - lib/uhaul/fetch_error.rb
106
106
  - lib/uhaul/geocode.rb
107
107
  - lib/uhaul/link.rb
108
+ - lib/uhaul/parse_error.rb
108
109
  - lib/uhaul/price.rb
109
110
  - lib/uhaul/rates.rb
110
111
  - lib/uhaul/sitemap.rb
@@ -115,8 +116,8 @@ licenses:
115
116
  metadata:
116
117
  rubygems_mfa_required: 'true'
117
118
  homepage_uri: https://github.com/ksylvest/uhaul
118
- source_code_uri: https://github.com/ksylvest/uhaul/tree/v1.1.0
119
- changelog_uri: https://github.com/ksylvest/uhaul/releases/tag/v1.1.0
119
+ source_code_uri: https://github.com/ksylvest/uhaul/tree/v1.2.0
120
+ changelog_uri: https://github.com/ksylvest/uhaul/releases/tag/v1.2.0
120
121
  documentation_uri: https://uhaul.ksylvest.com/
121
122
  post_install_message:
122
123
  rdoc_options: []