extraspace 0.1.1 → 0.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 +4 -4
- data/README.md +20 -16
- data/lib/extraspace/address.rb +10 -16
- data/lib/extraspace/crawler.rb +52 -0
- data/lib/extraspace/dimensions.rb +4 -11
- data/lib/extraspace/facility.rb +11 -4
- data/lib/extraspace/geocode.rb +1 -1
- data/lib/extraspace/link.rb +26 -0
- data/lib/extraspace/price.rb +8 -17
- data/lib/extraspace/rates.rb +2 -10
- data/lib/extraspace/sitemap.rb +43 -0
- data/lib/extraspace/version.rb +1 -1
- metadata +5 -3
- data/lib/extraspace/availability.rb +0 -30
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c428e33759133c5e280f2dfb72c99c04e71ee4c64f96f6b9ba2228cd5fdc033e
|
4
|
+
data.tar.gz: a2be67b968e35c3fa689583d00cacf92608cf74906d8b854c52f9333059a1857
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c66f9b36b6ee74d375a791c57a9f0712efe65c65a3aa5b6c7bc146779e22cb2f10cb5e2f176eea2b6430a7255620f0fa218d4bcefa015b39723c35653821ea15
|
7
|
+
data.tar.gz: d0f1f59b09c2f6d86baad0819ab63f5ca68491e9ccfc9895c0f5f43ccb69418130dc898ed1087ad9d1580397ad9270256f8253d61982c092969e1bbde27214dc
|
data/README.md
CHANGED
@@ -17,22 +17,26 @@ gem install extrapsace
|
|
17
17
|
```ruby
|
18
18
|
require 'extraspace'
|
19
19
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
puts "
|
27
|
-
puts "
|
28
|
-
puts "
|
29
|
-
puts "
|
30
|
-
puts
|
31
|
-
|
32
|
-
facility.prices.each do |price|
|
33
|
-
puts "UID: #{price.uid}"
|
34
|
-
puts "Dimensions: #{price.dimensions.display}"
|
35
|
-
puts "Rates: $#{price.rates.street} (street) / $#{price.rates.web} (web)"
|
20
|
+
sitemap = ExtraSpace::Facility.sitemap
|
21
|
+
sitemap.links.each do |link|
|
22
|
+
url = link.loc
|
23
|
+
|
24
|
+
facility = ExtraSpace::Facility.fetch(url:)
|
25
|
+
|
26
|
+
puts "Street: #{facility.address.street}"
|
27
|
+
puts "City: #{facility.address.city}"
|
28
|
+
puts "State: #{facility.address.state}"
|
29
|
+
puts "ZIP: #{facility.address.zip}"
|
30
|
+
puts "Latitude: #{facility.geocode.latitude}"
|
31
|
+
puts "Longitude: #{facility.geocode.longitude}"
|
36
32
|
puts
|
33
|
+
|
34
|
+
facility.prices.each do |price|
|
35
|
+
puts "ID: #{price.id}"
|
36
|
+
puts "Width: #{price.dimensions.width}"
|
37
|
+
puts "Depth: #{price.dimensions.depth}"
|
38
|
+
puts "Rates: $#{price.rates.street} (street) / $#{price.rates.web} (web)"
|
39
|
+
puts
|
40
|
+
end
|
37
41
|
end
|
38
42
|
```
|
data/lib/extraspace/address.rb
CHANGED
@@ -1,15 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ExtraSpace
|
4
|
-
#
|
4
|
+
# The address (street + city + state + zip) of a facility.
|
5
5
|
class Address
|
6
|
-
# @attribute [rw]
|
6
|
+
# @attribute [rw] street
|
7
7
|
# @return [String]
|
8
|
-
attr_accessor :
|
9
|
-
|
10
|
-
# @attribute [rw] line2
|
11
|
-
# @return [String]
|
12
|
-
attr_accessor :line2
|
8
|
+
attr_accessor :street
|
13
9
|
|
14
10
|
# @attribute [rw] city
|
15
11
|
# @return [String]
|
@@ -23,14 +19,12 @@ module ExtraSpace
|
|
23
19
|
# @return [String]
|
24
20
|
attr_accessor :zip
|
25
21
|
|
26
|
-
# @param
|
27
|
-
# @param line2 [String]
|
22
|
+
# @param street [String]
|
28
23
|
# @param city [String]
|
29
24
|
# @param state [String]
|
30
25
|
# @param zip [String]
|
31
|
-
def initialize(
|
32
|
-
@
|
33
|
-
@line2 = line2
|
26
|
+
def initialize(street:, city:, state:, zip:)
|
27
|
+
@street = street
|
34
28
|
@city = city
|
35
29
|
@state = state
|
36
30
|
@zip = zip
|
@@ -39,8 +33,7 @@ module ExtraSpace
|
|
39
33
|
# @return [String]
|
40
34
|
def inspect
|
41
35
|
props = [
|
42
|
-
"
|
43
|
-
"line2=#{@line2.inspect}",
|
36
|
+
"street=#{@street.inspect}",
|
44
37
|
"city=#{@city.inspect}",
|
45
38
|
"state=#{@state.inspect}",
|
46
39
|
"zip=#{@zip.inspect}"
|
@@ -52,9 +45,10 @@ module ExtraSpace
|
|
52
45
|
#
|
53
46
|
# @return [Address]
|
54
47
|
def self.parse(data:)
|
48
|
+
lines = %w[line1 line2 line3 line4].map { |key| data[key] }
|
49
|
+
|
55
50
|
new(
|
56
|
-
|
57
|
-
line2: data['line2'],
|
51
|
+
street: lines.compact.reject(&:empty?).join(' '),
|
58
52
|
city: data['city'],
|
59
53
|
state: data['stateName'],
|
60
54
|
zip: data['postalCode']
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExtraSpace
|
4
|
+
# Used to fetch and parse either HTML or XML via a URL.
|
5
|
+
class Crawler
|
6
|
+
# Raised for unexpected HTTP responses.
|
7
|
+
class FetchError < StandardError
|
8
|
+
# @param url [String]
|
9
|
+
# @param response [HTTP::Response]
|
10
|
+
def initialize(url:, response:)
|
11
|
+
super("url=#{url} status=#{response.status.inspect} body=#{response.body.inspect}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param url [String]
|
16
|
+
# @raise [FetchError]
|
17
|
+
# @return [Nokogiri::HTML::Document]
|
18
|
+
def self.html(url:)
|
19
|
+
new.html(url:)
|
20
|
+
end
|
21
|
+
|
22
|
+
# @param url [String]
|
23
|
+
# @raise [FetchError]
|
24
|
+
# @return [Nokogiri::XML::Document]
|
25
|
+
def self.xml(url:)
|
26
|
+
new.xml(url:)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @param url [String]
|
30
|
+
# @return [HTTP::Response]
|
31
|
+
def fetch(url:)
|
32
|
+
response = HTTP.get(url)
|
33
|
+
raise FetchError(url:, response: response.flush) unless response.status.ok?
|
34
|
+
|
35
|
+
response
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param url [String]
|
39
|
+
# @raise [FetchError]
|
40
|
+
# @return [Nokogiri::XML::Document]
|
41
|
+
def html(url:)
|
42
|
+
Nokogiri::HTML(String(fetch(url:).body))
|
43
|
+
end
|
44
|
+
|
45
|
+
# @param url [String]
|
46
|
+
# @raise [FetchError]
|
47
|
+
# @return [Nokogiri::XML::Document]
|
48
|
+
def xml(url:)
|
49
|
+
Nokogiri::XML(String(fetch(url:).body))
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ExtraSpace
|
4
|
-
#
|
4
|
+
# The dimensions (width + depth + sqft) of a price.
|
5
5
|
class Dimensions
|
6
6
|
# @attribute [rw] depth
|
7
7
|
# @return [Integer]
|
@@ -15,19 +15,13 @@ module ExtraSpace
|
|
15
15
|
# @return [Integer]
|
16
16
|
attr_accessor :sqft
|
17
17
|
|
18
|
-
# @attribute [rw] display
|
19
|
-
# @return [String]
|
20
|
-
attr_accessor :display
|
21
|
-
|
22
18
|
# @param depth [Integer]
|
23
19
|
# @param width [Integer]
|
24
20
|
# @param sqft [Integer]
|
25
|
-
|
26
|
-
def initialize(depth:, width:, sqft:, display:)
|
21
|
+
def initialize(depth:, width:, sqft:)
|
27
22
|
@depth = depth
|
28
23
|
@width = width
|
29
24
|
@sqft = sqft
|
30
|
-
@display = display
|
31
25
|
end
|
32
26
|
|
33
27
|
# @return [String]
|
@@ -35,8 +29,7 @@ module ExtraSpace
|
|
35
29
|
props = [
|
36
30
|
"depth=#{@depth.inspect}",
|
37
31
|
"width=#{@width.inspect}",
|
38
|
-
"sqft=#{@sqft.inspect}"
|
39
|
-
"display=#{@display.inspect}"
|
32
|
+
"sqft=#{@sqft.inspect}"
|
40
33
|
]
|
41
34
|
"#<#{self.class.name} #{props.join(' ')}>"
|
42
35
|
end
|
@@ -45,7 +38,7 @@ module ExtraSpace
|
|
45
38
|
#
|
46
39
|
# @return [Dimensions]
|
47
40
|
def self.parse(data:)
|
48
|
-
new(depth: data['depth'], width: data['width'], sqft: data['squareFoot']
|
41
|
+
new(depth: data['depth'], width: data['width'], sqft: data['squareFoot'])
|
49
42
|
end
|
50
43
|
end
|
51
44
|
end
|
data/lib/extraspace/facility.rb
CHANGED
@@ -1,8 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ExtraSpace
|
4
|
+
# A facility (address + geocode + prices) on extraspace.com.
|
5
|
+
#
|
4
6
|
# e.g. https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/
|
5
7
|
class Facility
|
8
|
+
SITEMAP_URL = 'https://www.extraspace.com/facility-sitemap.xml'
|
9
|
+
|
6
10
|
# @attribute [rw] address
|
7
11
|
# @return [Address]
|
8
12
|
attr_accessor :address
|
@@ -34,15 +38,18 @@ module ExtraSpace
|
|
34
38
|
"#<#{self.class.name} #{props.join(' ')}>"
|
35
39
|
end
|
36
40
|
|
41
|
+
# @return [Sitemap]
|
42
|
+
def self.sitemap
|
43
|
+
Sitemap.fetch(url: SITEMAP_URL)
|
44
|
+
end
|
45
|
+
|
37
46
|
# @param url [String]
|
38
47
|
#
|
39
48
|
# @return [Facility]
|
40
49
|
def self.fetch(url:)
|
41
|
-
|
42
|
-
document = Nokogiri::HTML(String(response.body))
|
50
|
+
document = Crawler.html(url:)
|
43
51
|
data = JSON.parse(document.at('#__NEXT_DATA__').text)
|
44
|
-
|
45
|
-
parse(data: data)
|
52
|
+
parse(data:)
|
46
53
|
end
|
47
54
|
|
48
55
|
# @param data [Hash]
|
data/lib/extraspace/geocode.rb
CHANGED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExtraSpace
|
4
|
+
# A link in a sitemap.
|
5
|
+
class Link
|
6
|
+
# @attribute [rw] loc
|
7
|
+
# @return [String]
|
8
|
+
attr_accessor :loc
|
9
|
+
|
10
|
+
# @attribute [rw] lastmod
|
11
|
+
# @return [Time]
|
12
|
+
attr_accessor :lastmod
|
13
|
+
|
14
|
+
# @param loc [String]
|
15
|
+
# @param lastmod [String]
|
16
|
+
def initialize(loc:, lastmod:)
|
17
|
+
@loc = loc
|
18
|
+
@lastmod = Time.parse(lastmod)
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
def inspect
|
23
|
+
"#<#{self.class.name} loc=#{@loc.inspect} lastmod=#{@lastmod.inspect}>"
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/extraspace/price.rb
CHANGED
@@ -1,15 +1,11 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ExtraSpace
|
4
|
-
#
|
4
|
+
# The price (id + dimensions + rate) for a facility
|
5
5
|
class Price
|
6
|
-
# @attribute [rw]
|
6
|
+
# @attribute [rw] id
|
7
7
|
# @return [String]
|
8
|
-
attr_accessor :
|
9
|
-
|
10
|
-
# @attribute [rw] availability
|
11
|
-
# @return [Availability]
|
12
|
-
attr_accessor :availability
|
8
|
+
attr_accessor :id
|
13
9
|
|
14
10
|
# @attribute [rw] dimensions
|
15
11
|
# @return [Dimensions]
|
@@ -19,13 +15,11 @@ module ExtraSpace
|
|
19
15
|
# @return [Rates]
|
20
16
|
attr_accessor :rates
|
21
17
|
|
22
|
-
# @param
|
23
|
-
# @param availability [Availability]
|
18
|
+
# @param id [String]
|
24
19
|
# @param dimensions [Dimensions]
|
25
20
|
# @param rates [Rates]
|
26
|
-
def initialize(
|
27
|
-
@
|
28
|
-
@availability = availability
|
21
|
+
def initialize(id:, dimensions:, rates:)
|
22
|
+
@id = id
|
29
23
|
@dimensions = dimensions
|
30
24
|
@rates = rates
|
31
25
|
end
|
@@ -33,8 +27,7 @@ module ExtraSpace
|
|
33
27
|
# @return [String]
|
34
28
|
def inspect
|
35
29
|
props = [
|
36
|
-
"
|
37
|
-
"availability=#{@availability.inspect}",
|
30
|
+
"id=#{@id.inspect}",
|
38
31
|
"dimensions=#{@dimensions.inspect}",
|
39
32
|
"rates=#{@rates.inspect}"
|
40
33
|
]
|
@@ -45,12 +38,10 @@ module ExtraSpace
|
|
45
38
|
#
|
46
39
|
# @return [Price]
|
47
40
|
def self.parse(data:)
|
48
|
-
availability = Availability.parse(data: data['availability'])
|
49
41
|
dimensions = Dimensions.parse(data: data['dimensions'])
|
50
42
|
rates = Rates.parse(data: data['rates'])
|
51
43
|
new(
|
52
|
-
|
53
|
-
availability: availability,
|
44
|
+
id: data['uid'],
|
54
45
|
dimensions: dimensions,
|
55
46
|
rates: rates
|
56
47
|
)
|
data/lib/extraspace/rates.rb
CHANGED
@@ -1,12 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ExtraSpace
|
4
|
-
#
|
4
|
+
# The rates (street + web) for a facility
|
5
5
|
class Rates
|
6
|
-
# @attribute [rw] nsc
|
7
|
-
# @return [Integer]
|
8
|
-
attr_accessor :nsc
|
9
|
-
|
10
6
|
# @attribute [rw] street
|
11
7
|
# @return [Integer]
|
12
8
|
attr_accessor :street
|
@@ -15,11 +11,9 @@ module ExtraSpace
|
|
15
11
|
# @return [Integer]
|
16
12
|
attr_accessor :web
|
17
13
|
|
18
|
-
# @param nsc [Integer]
|
19
14
|
# @param street [Integer]
|
20
15
|
# @param web [Integer]
|
21
|
-
def initialize(
|
22
|
-
@nsc = nsc
|
16
|
+
def initialize(street:, web:)
|
23
17
|
@street = street
|
24
18
|
@web = web
|
25
19
|
end
|
@@ -27,7 +21,6 @@ module ExtraSpace
|
|
27
21
|
# @return [String]
|
28
22
|
def inspect
|
29
23
|
props = [
|
30
|
-
"nsc=#{@nsc.inspect}",
|
31
24
|
"street=#{@street.inspect}",
|
32
25
|
"web=#{@web.inspect}"
|
33
26
|
]
|
@@ -39,7 +32,6 @@ module ExtraSpace
|
|
39
32
|
# @return [Rates]
|
40
33
|
def self.parse(data:)
|
41
34
|
new(
|
42
|
-
nsc: data['nsc'],
|
43
35
|
street: data['street'],
|
44
36
|
web: data['web']
|
45
37
|
)
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExtraSpace
|
4
|
+
# A sitemap on extraspace.com.
|
5
|
+
#
|
6
|
+
# e.g. https://www.extraspace.com/facility-sitemap.xml
|
7
|
+
class Sitemap
|
8
|
+
# @attribute [rw] links
|
9
|
+
# @return [Array<Link>]
|
10
|
+
attr_accessor :links
|
11
|
+
|
12
|
+
# @param document [NokoGiri::XML::Document]
|
13
|
+
#
|
14
|
+
# @return [Sitemap]
|
15
|
+
def self.parse(document:)
|
16
|
+
links = document.xpath('//xmlns:url').map do |url|
|
17
|
+
loc = url.at_xpath('xmlns:loc')&.text
|
18
|
+
lastmod = url.at_xpath('xmlns:lastmod')&.text
|
19
|
+
Link.new(loc:, lastmod:)
|
20
|
+
end
|
21
|
+
|
22
|
+
new(links: links)
|
23
|
+
end
|
24
|
+
|
25
|
+
# @param url [String]
|
26
|
+
#
|
27
|
+
# @return [Sitemap]
|
28
|
+
def self.fetch(url:)
|
29
|
+
document = Crawler.xml(url:)
|
30
|
+
parse(document:)
|
31
|
+
end
|
32
|
+
|
33
|
+
# @param links [Array<Link>]
|
34
|
+
def initialize(links:)
|
35
|
+
@links = links
|
36
|
+
end
|
37
|
+
|
38
|
+
# @return [String]
|
39
|
+
def inspect
|
40
|
+
"#<#{self.class.name} links=#{@links.inspect}>"
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
data/lib/extraspace/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: extraspace
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.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: 2024-11-
|
11
|
+
date: 2024-11-27 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http
|
@@ -79,12 +79,14 @@ files:
|
|
79
79
|
- bin/setup
|
80
80
|
- lib/extraspace.rb
|
81
81
|
- lib/extraspace/address.rb
|
82
|
-
- lib/extraspace/
|
82
|
+
- lib/extraspace/crawler.rb
|
83
83
|
- lib/extraspace/dimensions.rb
|
84
84
|
- lib/extraspace/facility.rb
|
85
85
|
- lib/extraspace/geocode.rb
|
86
|
+
- lib/extraspace/link.rb
|
86
87
|
- lib/extraspace/price.rb
|
87
88
|
- lib/extraspace/rates.rb
|
89
|
+
- lib/extraspace/sitemap.rb
|
88
90
|
- lib/extraspace/version.rb
|
89
91
|
homepage: https://github.com/ksylvest/extraspace
|
90
92
|
licenses:
|
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module ExtraSpace
|
4
|
-
# e.g. https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/
|
5
|
-
class Availability
|
6
|
-
# @attribute [rw] available
|
7
|
-
# @return [String]
|
8
|
-
attr_accessor :available
|
9
|
-
|
10
|
-
# @param available [String]
|
11
|
-
def initialize(available:)
|
12
|
-
@available = available
|
13
|
-
end
|
14
|
-
|
15
|
-
# @return [String]
|
16
|
-
def inspect
|
17
|
-
props = [
|
18
|
-
"available=#{@available.inspect}"
|
19
|
-
]
|
20
|
-
"#<#{self.class.name} #{props.join(' ')}>"
|
21
|
-
end
|
22
|
-
|
23
|
-
# @param data [Hash]
|
24
|
-
#
|
25
|
-
# @return [Availability]
|
26
|
-
def self.parse(data:)
|
27
|
-
new(available: data['available'])
|
28
|
-
end
|
29
|
-
end
|
30
|
-
end
|