extraspace 0.1.2 → 0.3.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: 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