extraspace 0.1.2 → 0.3.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: b2d656b5fbca3894562492b6a8c3e90c97a4c785cf88d6ff7c2c65c380a5ba8e
4
- data.tar.gz: c564ac28246067bfb3d843070aa8ea3dcaa0898e12161e15c7570439cd8d4907
3
+ metadata.gz: e31518705b665cfcff3914a8553164f1463a3c66140cb41ea14be9154abf44fa
4
+ data.tar.gz: bf86b8674cf0557e239b286acaca2f77d1688bd3f431434ad15914db9624a143
5
5
  SHA512:
6
- metadata.gz: 503b60e74410c4fe5cf0db8644382555cf0ddb8343790e5eed69d61c7b8d5e494be3cdbd55097fee3454f555f2bd030722b704906384da65d020e688252ad46a
7
- data.tar.gz: d48d92e05c00b065619738803344b79254ecb29cec4e9d461a47709169c1125864d278f07ce421647b17d2e6f83efdb09c9a102fe393766edab8d5aa457639db
6
+ metadata.gz: 6204e8de2f7349debac7049c5ca1ade4fc0b5328be5faaf61eefb42d982e735897c28fc6292ac98c6a40e36dadf94550fcd30f5a066b3eb58ba8de7db3c2c20d
7
+ data.tar.gz: edd94f3a7d6dadc415f112f7dd3fa1ff7e8ae5fd8592794e3a218a3806e5ec7777659eeaee3bde305af51eee67f23fb5b5862925f51fc54d22ff5aa5ad863f34
data/README.md CHANGED
@@ -20,23 +20,20 @@ require 'extraspace'
20
20
  sitemap = ExtraSpace::Facility.sitemap
21
21
  sitemap.links.each do |link|
22
22
  url = link.loc
23
-
24
23
  facility = ExtraSpace::Facility.fetch(url:)
25
24
 
26
- puts "Line 1: #{facility.address.line1}"
27
- puts "Line 2: #{facility.address.line2}"
28
- puts "City: #{facility.address.city}"
29
- puts "State: #{facility.address.state}"
30
- puts "ZIP: #{facility.address.zip}"
31
- puts "Latitude: #{facility.geocode.latitude}"
32
- puts "Longitude: #{facility.geocode.longitude}"
33
- puts
25
+ puts facility.text
34
26
 
35
27
  facility.prices.each do |price|
36
- puts "UID: #{price.uid}"
37
- puts "Dimensions: #{price.dimensions.display}"
38
- puts "Rates: $#{price.rates.street} (street) / $#{price.rates.web} (web)"
39
- puts
28
+ puts price.text
40
29
  end
30
+
31
+ puts
41
32
  end
42
33
  ```
34
+
35
+ ## CLI
36
+
37
+ ```bash
38
+ extraspace crawl
39
+ ```
data/exe/extraspace ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'extraspace'
5
+
6
+ cli = ExtraSpace::CLI.new
7
+ cli.parse
@@ -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}"
@@ -48,13 +41,19 @@ module ExtraSpace
48
41
  "#<#{self.class.name} #{props.join(' ')}>"
49
42
  end
50
43
 
44
+ # @return [String]
45
+ def text
46
+ "#{street}, #{city}, #{state} #{zip}"
47
+ end
48
+
51
49
  # @param data [Hash]
52
50
  #
53
51
  # @return [Address]
54
52
  def self.parse(data:)
53
+ lines = %w[line1 line2 line3 line4].map { |key| data[key] }
54
+
55
55
  new(
56
- line1: data['line1'],
57
- line2: data['line2'],
56
+ street: lines.compact.reject(&:empty?).join(' '),
58
57
  city: data['city'],
59
58
  state: data['stateName'],
60
59
  zip: data['postalCode']
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+
5
+ module ExtraSpace
6
+ # Used when interacting with the library from the command line interface (CLI).
7
+ #
8
+ # Usage:
9
+ #
10
+ # cli = ExtraSpace::CLI.new
11
+ # cli.parse
12
+ class CLI
13
+ module Code
14
+ OK = 0
15
+ ERROR = 1
16
+ end
17
+
18
+ # @param argv [Array<String>]
19
+ def parse(argv = ARGV)
20
+ parser.parse!(argv)
21
+ command = argv.shift
22
+
23
+ case command
24
+ when 'crawl' then crawl
25
+ else
26
+ warn("unsupported command=#{command.inspect}")
27
+ exit(Code::ERROR)
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ def crawl
34
+ ExtraSpace::Facility.crawl
35
+ exit(Code::OK)
36
+ end
37
+
38
+ def help(options)
39
+ puts(options)
40
+ exit(Code::OK)
41
+ end
42
+
43
+ def version
44
+ puts(VERSION)
45
+ exit(Code::OK)
46
+ end
47
+
48
+ # @return [OptionParser]
49
+ def parser
50
+ OptionParser.new do |options|
51
+ options.banner = 'usage: extraspace [options] <command> [<args>]'
52
+
53
+ options.on('-h', '--help', 'help') { help(options) }
54
+ options.on('-v', '--version', 'version') { version }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -8,7 +8,7 @@ module ExtraSpace
8
8
  # @param url [String]
9
9
  # @param response [HTTP::Response]
10
10
  def initialize(url:, response:)
11
- super("url=#{url} status=#{response.status.inspect} body=#{response.body.inspect}")
11
+ super("url=#{url} status=#{response.status.inspect} body=#{String(response.body).inspect}")
12
12
  end
13
13
  end
14
14
 
@@ -30,7 +30,7 @@ module ExtraSpace
30
30
  # @return [HTTP::Response]
31
31
  def fetch(url:)
32
32
  response = HTTP.get(url)
33
- raise FetchError(url:, response: response.flush) unless response.status.ok?
33
+ raise FetchError.new(url:, response: response.flush) unless response.status.ok?
34
34
 
35
35
  response
36
36
  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,17 +29,21 @@ 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
43
36
 
37
+ # @return [String] e.g. "10' × 10' (100 sqft)"
38
+ def text
39
+ "#{format('%g', @width)}' × #{format('%g', @depth)}' (#{@sqft} sqft)"
40
+ end
41
+
44
42
  # @param data [Hash]
45
43
  #
46
44
  # @return [Dimensions]
47
45
  def self.parse(data:)
48
- new(depth: data['depth'], width: data['width'], sqft: data['squareFoot'], display: data['display'])
46
+ new(depth: data['depth'], width: data['width'], sqft: data['squareFoot'])
49
47
  end
50
48
  end
51
49
  end
@@ -1,10 +1,20 @@
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
6
8
  SITEMAP_URL = 'https://www.extraspace.com/facility-sitemap.xml'
7
9
 
10
+ # @attribute [rw] id
11
+ # @return [String]
12
+ attr_accessor :id
13
+
14
+ # @attribute [rw] name
15
+ # @return [String]
16
+ attr_accessor :name
17
+
8
18
  # @attribute [rw] address
9
19
  # @return [Address]
10
20
  attr_accessor :address
@@ -17,25 +27,6 @@ module ExtraSpace
17
27
  # @return [Array<Price>]
18
28
  attr_accessor :prices
19
29
 
20
- # @param address [Address]
21
- # @param geocode [Geocode]
22
- # @param prices [Array<Price>]
23
- def initialize(address:, geocode:, prices:)
24
- @address = address
25
- @geocode = geocode
26
- @prices = prices
27
- end
28
-
29
- # @return [String]
30
- def inspect
31
- props = [
32
- "address=#{@address.inspect}",
33
- "geocode=#{@geocode.inspect}",
34
- "prices=#{@prices.inspect}"
35
- ]
36
- "#<#{self.class.name} #{props.join(' ')}>"
37
- end
38
-
39
30
  # @return [Sitemap]
40
31
  def self.sitemap
41
32
  Sitemap.fetch(url: SITEMAP_URL)
@@ -55,14 +46,60 @@ module ExtraSpace
55
46
  # @return [Facility]
56
47
  def self.parse(data:)
57
48
  page_data = data.dig('props', 'pageProps', 'pageData', 'data')
58
- facility_data = page_data.dig('facilityData', 'data')
49
+ store_data = page_data.dig('facilityData', 'data', 'store')
59
50
  unit_classes = page_data.dig('unitClasses', 'data', 'unitClasses')
51
+ id = store_data['number']
52
+ name = store_data['name']
60
53
 
61
- address = Address.parse(data: facility_data['store']['address'])
62
- geocode = Geocode.parse(data: facility_data['store']['geocode'])
54
+ address = Address.parse(data: store_data['address'])
55
+ geocode = Geocode.parse(data: store_data['geocode'])
63
56
  prices = unit_classes.map { |price_data| Price.parse(data: price_data) }
64
57
 
65
- new(address:, geocode:, prices:)
58
+ new(id:, name:, address:, geocode:, prices:)
59
+ end
60
+
61
+ def self.crawl
62
+ sitemap.links.each do |link|
63
+ url = link.loc
64
+
65
+ facility = fetch(url:)
66
+ puts facility.text
67
+
68
+ facility.prices.each do |price|
69
+ puts price.text
70
+ end
71
+
72
+ puts
73
+ end
74
+ end
75
+
76
+ # @param id [String]
77
+ # @param name [String]
78
+ # @param address [Address]
79
+ # @param geocode [Geocode]
80
+ # @param prices [Array<Price>]
81
+ def initialize(id:, name:, address:, geocode:, prices:)
82
+ @id = id
83
+ @name = name
84
+ @address = address
85
+ @geocode = geocode
86
+ @prices = prices
87
+ end
88
+
89
+ # @return [String]
90
+ def inspect
91
+ props = [
92
+ "id=#{@id.inspect}",
93
+ "address=#{@address.inspect}",
94
+ "geocode=#{@geocode.inspect}",
95
+ "prices=#{@prices.inspect}"
96
+ ]
97
+ "#<#{self.class.name} #{props.join(' ')}>"
98
+ end
99
+
100
+ # @return [String] e.g. "123 Main St, Springfield, IL 62701"
101
+ def text
102
+ "#{@id} | #{@name} | #{@address.text} | #{@geocode.text}"
66
103
  end
67
104
  end
68
105
  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 geocode (latitude + longitude) of a facility.
5
5
  class Geocode
6
6
  # @attribute [rw] latitude
7
7
  # @return [Float]
@@ -27,6 +27,11 @@ module ExtraSpace
27
27
  "#<#{self.class.name} #{props.join(' ')}>"
28
28
  end
29
29
 
30
+ # @return [String]
31
+ def text
32
+ "#{@latitude},#{@longitude}"
33
+ end
34
+
30
35
  # @param data [Hash]
31
36
  #
32
37
  # @return [Geocode]
@@ -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,24 +27,26 @@ 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
  ]
41
34
  "#<#{self.class.name} #{props.join(' ')}>"
42
35
  end
43
36
 
37
+ # @return [String] e.g. "123 | 5' × 5' (25 sqft) | $100 (street) / $90 (web)"
38
+ def text
39
+ "#{@id} | #{@dimensions.text} | #{@rates.text}"
40
+ end
41
+
44
42
  # @param data [Hash]
45
43
  #
46
44
  # @return [Price]
47
45
  def self.parse(data:)
48
- availability = Availability.parse(data: data['availability'])
49
46
  dimensions = Dimensions.parse(data: data['dimensions'])
50
47
  rates = Rates.parse(data: data['rates'])
51
48
  new(
52
- uid: data['uid'],
53
- availability: availability,
49
+ id: data['uid'],
54
50
  dimensions: dimensions,
55
51
  rates: rates
56
52
  )
@@ -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,19 +21,22 @@ 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
  ]
34
27
  "#<#{self.class.name} #{props.join(' ')}>"
35
28
  end
36
29
 
30
+ # @return [String] e.g. "$80 (street) | $60 (web)"
31
+ def text
32
+ "$#{@street} (street) | $#{@web} (web)"
33
+ end
34
+
37
35
  # @param data [Hash]
38
36
  #
39
37
  # @return [Rates]
40
38
  def self.parse(data:)
41
39
  new(
42
- nsc: data['nsc'],
43
40
  street: data['street'],
44
41
  web: data['web']
45
42
  )
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExtraSpace
4
+ # A sitemap on extraspace.com.
5
+ #
4
6
  # e.g. https://www.extraspace.com/facility-sitemap.xml
5
7
  class Sitemap
6
8
  # @attribute [rw] links
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ExtraSpace
4
- VERSION = '0.1.2'
4
+ VERSION = '0.3.0'
5
5
  end
data/lib/extraspace.rb CHANGED
@@ -6,6 +6,7 @@ require 'zeitwerk'
6
6
 
7
7
  loader = Zeitwerk::Loader.for_gem
8
8
  loader.inflector.inflect 'extraspace' => 'ExtraSpace'
9
+ loader.inflector.inflect 'cli' => 'CLI'
9
10
  loader.setup
10
11
 
11
12
  module ExtraSpace
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.2
4
+ version: 0.3.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-26 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
@@ -52,6 +52,20 @@ dependencies:
52
52
  - - ">="
53
53
  - !ruby/object:Gem::Version
54
54
  version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: optparse
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
55
69
  - !ruby/object:Gem::Dependency
56
70
  name: zeitwerk
57
71
  requirement: !ruby/object:Gem::Requirement
@@ -69,7 +83,8 @@ dependencies:
69
83
  description: Uses HTTP.rb to scrape extraspace.com.
70
84
  email:
71
85
  - kevin@ksylvest.com
72
- executables: []
86
+ executables:
87
+ - extraspace
73
88
  extensions: []
74
89
  extra_rdoc_files: []
75
90
  files:
@@ -77,9 +92,10 @@ files:
77
92
  - README.md
78
93
  - bin/console
79
94
  - bin/setup
95
+ - exe/extraspace
80
96
  - lib/extraspace.rb
81
97
  - lib/extraspace/address.rb
82
- - lib/extraspace/availability.rb
98
+ - lib/extraspace/cli.rb
83
99
  - lib/extraspace/crawler.rb
84
100
  - lib/extraspace/dimensions.rb
85
101
  - lib/extraspace/facility.rb
@@ -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