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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 5c85ab034de8c329973cfe53e470745608a52076d9dce4a037532dc8214f5913
4
- data.tar.gz: fa535b917b40c8349b79683642ce5e367b0ea34c519fbfd16c094fba87025ebe
3
+ metadata.gz: c428e33759133c5e280f2dfb72c99c04e71ee4c64f96f6b9ba2228cd5fdc033e
4
+ data.tar.gz: a2be67b968e35c3fa689583d00cacf92608cf74906d8b854c52f9333059a1857
5
5
  SHA512:
6
- metadata.gz: d243f120d52703c773053469004d735eb8deadd63f0706012f1b4e92abe2fc747db2010f8a99e561cce4336841b3e350ebe27376a2de1f037fce75d6da14935c
7
- data.tar.gz: 21db75d1f583a0577bc4b62ea4465eca29e69185f668b254c5fb1ee19dc4215ebb77f2abdf3234820e06964b4bc1926255b74a36b7619017cf203ea0a68a8b89
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
- URL = 'https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/'
21
- facility = ExtraSpace::Facility.fetch(url: URL)
22
-
23
- puts "Line 1: #{facility.address.line1}"
24
- puts "Line 2: #{facility.address.line2}"
25
- puts "City: #{facility.address.city}"
26
- puts "State: #{facility.address.state}"
27
- puts "ZIP: #{facility.address.zip}"
28
- puts "Latitude: #{facility.geocode.latitude}"
29
- puts "Longitude: #{facility.geocode.longitude}"
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
  ```
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExtraSpace
4
- # e.g. https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/
4
+ # The address (street + city + state + zip) of a facility.
5
5
  class Address
6
- # @attribute [rw] line1
6
+ # @attribute [rw] street
7
7
  # @return [String]
8
- attr_accessor :line1
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 line1 [String]
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(line1:, line2:, city:, state:, zip:)
32
- @line1 = line1
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
- "line1=#{@line1.inspect}",
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
- line1: data['line1'],
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
- # e.g. https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/
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
- # @param display [String]
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'], display: data['display'])
41
+ new(depth: data['depth'], width: data['width'], sqft: data['squareFoot'])
49
42
  end
50
43
  end
51
44
  end
@@ -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
- response = HTTP.get(url)
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]
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExtraSpace
4
- # e.g. https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/
4
+ # The geocode (latitude + longitude) of a facility.
5
5
  class Geocode
6
6
  # @attribute [rw] latitude
7
7
  # @return [Float]
@@ -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
@@ -1,15 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExtraSpace
4
- # e.g. https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/
4
+ # The price (id + dimensions + rate) for a facility
5
5
  class Price
6
- # @attribute [rw] uid
6
+ # @attribute [rw] id
7
7
  # @return [String]
8
- attr_accessor :uid
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 uid [String]
23
- # @param availability [Availability]
18
+ # @param id [String]
24
19
  # @param dimensions [Dimensions]
25
20
  # @param rates [Rates]
26
- def initialize(uid:, availability:, dimensions:, rates:)
27
- @uid = uid
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
- "uid=#{@uid.inspect}",
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
- uid: data['uid'],
53
- availability: availability,
44
+ id: data['uid'],
54
45
  dimensions: dimensions,
55
46
  rates: rates
56
47
  )
@@ -1,12 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExtraSpace
4
- # e.g. https://www.extraspace.com/storage/facilities/us/alabama/auburn/3264/
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(nsc:, street:, web:)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExtraSpace
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
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.1.1
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-21 00:00:00.000000000 Z
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/availability.rb
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