nsastorage 0.1.0 → 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: ecae367c1dc75c720c89af10a90e6325bfe10c03dbfee675a125a45c70fcc6f5
4
- data.tar.gz: dbd35a90238e4a806a9e569bae4c99f474f101d92e906eef91cec2a7c85867b2
3
+ metadata.gz: 6bf4dc5114c043fc32d886eb98d630f24bfff9d3ccefbdd3a92b006c3cb967d4
4
+ data.tar.gz: c81742bdf7d0d35c5dc23b2584ad10d47b370a1de6e564e08233ed9db7b21538
5
5
  SHA512:
6
- metadata.gz: 3fa08993d81f797354e095db5f367e650a4c58c67763fa147f65de1cc4794113c7b4394bb5656219484ac62dd530c4dc70fc5b2aa86a6b826140b0c1cd767575
7
- data.tar.gz: 2b0dbea4c879f0d44d0716772830b330d497ee1c8170969909db7dcdb375203b50672cba4d1f4611e29e3cd800e183aeee405e4f90b040b5fc7994a936691695
6
+ metadata.gz: 801e52c62bdac13fcc9338496e47d866bb8392cf7b988b2535596963c5030bc90a5400ce7ad173d71e4a7cac61a89c0a196f0d064115431219fe5301167b9f2e
7
+ data.tar.gz: ad2da5dfc46c674e85bc310312f6e30feb65f19df3036ce0ba91ee16bd5676c30fe1e5d75241a2e0bbaf7fcd79d67034360455cd00f95ebcf3f31294feff0d94
@@ -3,6 +3,8 @@
3
3
  module NSAStorage
4
4
  # The address (street + city + state + zip) of a facility.
5
5
  class Address
6
+ ADDRESS_SELECTOR = '.item-des-box .text-box i'
7
+ ADDRESS_REGEX = /(?<street>.+),\s+(?<city>.+),\s+(?<state>.+)\s+(?<zip>\d{5})/
6
8
  # @attribute [rw] street
7
9
  # @return [String]
8
10
  attr_accessor :street
@@ -46,15 +48,17 @@ module NSAStorage
46
48
  "#{street}, #{city}, #{state} #{zip}"
47
49
  end
48
50
 
49
- # @param data [Hash]
51
+ # @param document [Nokogiri::HTML::Document]
50
52
  #
51
53
  # @return [Address]
52
- def self.parse(data:)
54
+ def self.parse(document:)
55
+ element = document.at_css(ADDRESS_SELECTOR)
56
+ match = element.text.match(ADDRESS_REGEX)
53
57
  new(
54
- street: data['streetAddress'],
55
- city: data['addressLocality'],
56
- state: data['addressRegion'],
57
- zip: data['postalCode']
58
+ street: match[:street],
59
+ city: match[:city],
60
+ state: match[:state],
61
+ zip: match[:zip]
58
62
  )
59
63
  end
60
64
  end
@@ -5,6 +5,13 @@ module NSAStorage
5
5
  class Crawler
6
6
  HOST = 'https://www.nsastorage.com'
7
7
 
8
+ # @attribute url [String]
9
+ # @raise [FetchError]
10
+ # @return [Hash]
11
+ def self.json(url:)
12
+ new.json(url:)
13
+ end
14
+
8
15
  # @param url [String]
9
16
  # @raise [FetchError]
10
17
  # @return [Nokogiri::HTML::Document]
@@ -42,6 +49,13 @@ module NSAStorage
42
49
  response
43
50
  end
44
51
 
52
+ # @param url [String]
53
+ # @raise [FetchError]
54
+ # @return [Hash]
55
+ def json(url:)
56
+ JSON.parse(String(fetch(url:).body))
57
+ end
58
+
45
59
  # @param url [String]
46
60
  # @raise [FetchError]
47
61
  # @return [Nokogiri::XML::Document]
@@ -5,6 +5,8 @@ module NSAStorage
5
5
  class Dimensions
6
6
  DEFAULT_HEIGHT = 8.0 # feet
7
7
 
8
+ DIMENSIONS_REGEX = /(?<width>[\d\.]+) x (?<depth>[\d\.]+)/
9
+
8
10
  # @attribute [rw] depth
9
11
  # @return [Float]
10
12
  attr_accessor :depth
@@ -55,9 +57,8 @@ module NSAStorage
55
57
  #
56
58
  # @return [Dimensions]
57
59
  def self.parse(element:)
58
- text = element.text
59
- match = text.match(/(?<width>[\d\.]+)'x(?<depth>[\d\.]+)'/)
60
- raise text.inspect if match.nil?
60
+ text = element.at_css('.unit-select-item-detail').text
61
+ match = DIMENSIONS_REGEX.match(text)
61
62
 
62
63
  width = Float(match[:width])
63
64
  depth = Float(match[:depth])
@@ -7,13 +7,11 @@ module NSAStorage
7
7
  class Facility
8
8
  class ParseError < StandardError; end
9
9
 
10
- DEFAULT_EMAIL = 'TODO'
11
- DEFAULT_PHONE = 'TODO'
10
+ DEFAULT_EMAIL = 'customerservice@nsabrands.com'
11
+ DEFAULT_PHONE = '+1-844-434-1150'
12
12
 
13
13
  SITEMAP_URL = 'https://www.nsastorage.com/sitemap.xml'
14
14
 
15
- ID_REGEX = %r{/(?<id>\d+)}
16
-
17
15
  # @attribute [rw] id
18
16
  # @return [String]
19
17
  attr_accessor :id
@@ -64,13 +62,14 @@ module NSAStorage
64
62
  #
65
63
  # @return [Facility]
66
64
  def self.parse(url:, document:)
67
- data = parse_json_ld(document: document)
68
- id = ID_REGEX.match(url)[:id]
65
+ id = Integer(document.at_css('[data-facility-id]')['data-facility-id'])
66
+ name = document.at_css('.section-title').text.strip
69
67
 
70
- address = Address.parse(data: data['address'])
71
- geocode = Geocode.parse(data: data['address'])
68
+ geocode = Geocode.parse(document:)
69
+ address = Address.parse(document:)
70
+ prices = Price.fetch(facility_id: id)
72
71
 
73
- new(id:, url:, name: data['name'], address:, geocode:)
72
+ new(id:, url:, name:, address:, geocode:, prices:)
74
73
  end
75
74
 
76
75
  # @param document [Nokogiri::HTML::Document]
@@ -78,11 +77,19 @@ module NSAStorage
78
77
  # @raise [ParseError]
79
78
  #
80
79
  # @return [Hash]
81
- def self.parse_json_ld(document:)
82
- document
83
- .xpath('//script[@type="application/ld+json"]')
84
- .map { |script| JSON.parse(script.text) }
85
- .find { |data| data['@type'] == 'SelfStorage' }
80
+ def self.parse_ld_json_script(document:)
81
+ parse_ld_json_scripts(document:).find do |data|
82
+ data['@type'] == 'SelfStorage'
83
+ end || raise(ParseError, 'missing ld+json')
84
+ end
85
+
86
+ # @param document [Nokogiri::HTML::Document]
87
+ #
88
+ # @return [Array<Hash>]
89
+ def self.parse_ld_json_scripts(document:)
90
+ elements = document.xpath('//script[@type="application/ld+json"]')
91
+
92
+ elements.map { |element| element.text.empty? ? {} : JSON.parse(element.text) }
86
93
  end
87
94
 
88
95
  # @param id [String]
@@ -11,20 +11,17 @@ module NSAStorage
11
11
 
12
12
  new(
13
13
  climate_controlled: text.include?('Climate controlled'),
14
- inside_drive_up_access: text.include?('Inside drive-up access'),
15
- outside_drive_up_access: text.include?('Outside drive-up access'),
16
- first_floor_access: text.include?('1st floor access')
14
+ drive_up_access: text.include?('Drive Up Access'),
15
+ first_floor_access: text.include?('1st Floor')
17
16
  )
18
17
  end
19
18
 
20
19
  # @param climate_controlled [Boolean]
21
- # @param inside_drive_up_access [Boolean]
22
- # @param outside_drive_up_access [Boolean]
20
+ # @param drive_up_access [Boolean]
23
21
  # @param first_floor_access [Boolean]
24
- def initialize(climate_controlled:, inside_drive_up_access:, outside_drive_up_access:, first_floor_access:)
22
+ def initialize(climate_controlled:, drive_up_access:, first_floor_access:)
25
23
  @climate_controlled = climate_controlled
26
- @inside_drive_up_access = inside_drive_up_access
27
- @outside_drive_up_access = outside_drive_up_access
24
+ @drive_up_access = drive_up_access
28
25
  @first_floor_access = first_floor_access
29
26
  end
30
27
 
@@ -32,8 +29,7 @@ module NSAStorage
32
29
  def inspect
33
30
  props = [
34
31
  "climate_controlled=#{@climate_controlled}",
35
- "inside_drive_up_access=#{@inside_drive_up_access}",
36
- "outside_drive_up_access=#{@outside_drive_up_access}",
32
+ "drive_up_access=#{@drive_up_access}",
37
33
  "first_floor_access=#{@first_floor_access}"
38
34
  ]
39
35
 
@@ -49,8 +45,7 @@ module NSAStorage
49
45
  def amenities
50
46
  [].tap do |amenities|
51
47
  amenities << 'Climate Controlled' if climate_controlled?
52
- amenities << 'Inside Drive-Up Access' if inside_drive_up_access?
53
- amenities << 'Outside Drive-Up Access' if outside_drive_up_access?
48
+ amenities << 'Drive-Up Access' if drive_up_access?
54
49
  amenities << 'First Floor Access' if first_floor_access?
55
50
  end
56
51
  end
@@ -60,19 +55,9 @@ module NSAStorage
60
55
  @climate_controlled
61
56
  end
62
57
 
63
- # @return [Boolean]
64
- def inside_drive_up_access?
65
- @inside_drive_up_access
66
- end
67
-
68
- # @return [Boolean]
69
- def outside_drive_up_access?
70
- @outside_drive_up_access
71
- end
72
-
73
58
  # @return [Boolean]
74
59
  def drive_up_access?
75
- inside_drive_up_access? || outside_drive_up_access?
60
+ @drive_up_access
76
61
  end
77
62
 
78
63
  # @return [Boolean]
@@ -3,6 +3,9 @@
3
3
  module NSAStorage
4
4
  # The geocode (latitude + longitude) of a facility.
5
5
  class Geocode
6
+ LATITUDE_REGEX = /\\u0022lat\\u0022:(?<latitude>[\+\-\d\.]+)/
7
+ LONGITUDE_REGEX = /\\u0022long\\u0022:(?<longitude>[\+\-\d\.]+)/
8
+
6
9
  # @attribute [rw] latitude
7
10
  # @return [Float]
8
11
  attr_accessor :latitude
@@ -11,14 +14,14 @@ module NSAStorage
11
14
  # @return [Float]
12
15
  attr_accessor :longitude
13
16
 
14
- # @param data [Hash]
17
+ # @param document [Nokogiri::HTML::Document]
15
18
  #
16
19
  # @return [Geocode]
17
- def self.parse(data:)
18
- new(
19
- latitude: data['latitude'],
20
- longitude: data['longitude']
21
- )
20
+ def self.parse(document:)
21
+ latitude = LATITUDE_REGEX.match(document.text)[:latitude]
22
+ longitude = LONGITUDE_REGEX.match(document.text)[:longitude]
23
+
24
+ new(latitude: Float(latitude), longitude: Float(longitude))
22
25
  end
23
26
 
24
27
  # @param latitude [Float]
@@ -1,8 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NSAStorage
4
- # The price (id + dimensions + rate) for a facility
4
+ # The price (id + dimensions + rate) for a facility.
5
5
  class Price
6
+ ID_REGEX = %r{(?<id>\d+)/rent/}
6
7
  # @attribute [rw] id
7
8
  # @return [String]
8
9
  attr_accessor :id
@@ -19,6 +20,15 @@ module NSAStorage
19
20
  # @return [Rates]
20
21
  attr_accessor :rates
21
22
 
23
+ # @param facility_id [Integer]
24
+ #
25
+ # @return [Array<Price>]
26
+ def self.fetch(facility_id:)
27
+ url = "https://www.nsastorage.com/facility-units/#{facility_id}"
28
+ html = Crawler.json(url:)['data']['html']['units']
29
+ Nokogiri::HTML(html).css('[data-unit-size]').map { |element| parse(element:) }
30
+ end
31
+
22
32
  # @param id [String]
23
33
  # @param dimensions [Dimensions]
24
34
  # @param features [Features]
@@ -50,8 +60,9 @@ module NSAStorage
50
60
  #
51
61
  # @return [Price]
52
62
  def self.parse(element:)
63
+ link = element.at_xpath("//a[contains(text(), 'Rent')]")
53
64
  new(
54
- id: element.attr('id'),
65
+ id: ID_REGEX.match(link['href'])[:id],
55
66
  dimensions: Dimensions.parse(element:),
56
67
  features: Features.parse(element:),
57
68
  rates: Rates.parse(element:)
@@ -3,8 +3,8 @@
3
3
  module NSAStorage
4
4
  # The rates (street + web) for a facility
5
5
  class Rates
6
- STREET_SELECTOR = '.ptOriginalPriceSpan'
7
- WEB_SELECTOR = '.ptDiscountPriceSpan'
6
+ STREET_SELECTOR = '.part_item_old_price'
7
+ WEB_SELECTOR = '.part_item_price'
8
8
  VALUE_REGEX = /(?<value>[\d\.]+)/
9
9
 
10
10
  # @attribute [rw] street
@@ -19,7 +19,7 @@ module NSAStorage
19
19
  Link.new(loc:, lastmod:)
20
20
  end
21
21
 
22
- new(links: links)
22
+ new(links: links.filter { |link| link.loc.match(%r{/storage/}) })
23
23
  end
24
24
 
25
25
  # @param url [String]
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NSAStorage
4
- VERSION = '0.1.0'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,11 +1,11 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: nsastorage
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Sylvestre
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
11
  date: 2024-12-11 00:00:00.000000000 Z
@@ -117,7 +117,7 @@ metadata:
117
117
  homepage_uri: https://github.com/ksylvest/nsastorage
118
118
  source_code_uri: https://github.com/ksylvest/nsastorage
119
119
  changelog_uri: https://github.com/ksylvest/nsastorage
120
- post_install_message:
120
+ post_install_message:
121
121
  rdoc_options: []
122
122
  require_paths:
123
123
  - lib
@@ -132,8 +132,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
132
132
  - !ruby/object:Gem::Version
133
133
  version: '0'
134
134
  requirements: []
135
- rubygems_version: 3.5.22
136
- signing_key:
135
+ rubygems_version: 3.5.23
136
+ signing_key:
137
137
  specification_version: 4
138
138
  summary: A crawler for NSAStorage.
139
139
  test_files: []