extraspace 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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