cubesmart 0.1.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 +7 -0
- data/Gemfile +15 -0
- data/README.md +50 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/exe/cubesmart +7 -0
- data/lib/cubesmart/address.rb +63 -0
- data/lib/cubesmart/cli.rb +58 -0
- data/lib/cubesmart/config.rb +19 -0
- data/lib/cubesmart/crawler.rb +66 -0
- data/lib/cubesmart/dimensions.rb +49 -0
- data/lib/cubesmart/facility.rb +116 -0
- data/lib/cubesmart/geocode.rb +45 -0
- data/lib/cubesmart/link.rb +26 -0
- data/lib/cubesmart/price.rb +55 -0
- data/lib/cubesmart/rates.rb +45 -0
- data/lib/cubesmart/sitemap.rb +43 -0
- data/lib/cubesmart/version.rb +5 -0
- data/lib/cubesmart.rb +26 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: ea9ec6ccaa984aec55a381edbdb9bf8998b2d2e60274ae4440f6a6ee7476730e
|
4
|
+
data.tar.gz: 787a788d8d62665887f49dd8ae34e6f9ff6d0f0d3f31b507602e719a4d826e95
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9e68c9af8d7bef159d0a0c920fefe68dcdfa2bc3ac87bbf81217bc7f9a42e7520d8a154135a2e05b52530becf577b6ce9adeae53c15cee18306098f5b2affb6e
|
7
|
+
data.tar.gz: ad330358eb510641de1d7f6c94580895730ff443074d38babf561f0625fc093d220630c56a6ba3a4a19c47d53507c887a1db399d6df8ddaf3e438d30ae03511b
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,50 @@
|
|
1
|
+
# CubeSmart
|
2
|
+
|
3
|
+
[](https://github.com/ksylvest/cubesmart/blob/main/LICENSE)
|
4
|
+
[](https://rubygems.org/gems/cubesmart)
|
5
|
+
[](https://github.com/ksylvest/cubesmart)
|
6
|
+
[](https://cubesmart.ksylvest.com)
|
7
|
+
[](https://circleci.com/gh/ksylvest/cubesmart)
|
8
|
+
|
9
|
+
## Installation
|
10
|
+
|
11
|
+
```bash
|
12
|
+
gem install extrapsace
|
13
|
+
```
|
14
|
+
|
15
|
+
## Configuration
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'cubesmart'
|
19
|
+
|
20
|
+
CubeSmart.configure do |config|
|
21
|
+
config.user_agent = '../..' # ENV['CUBESMART_USER_AGENT']
|
22
|
+
config.timeout = 30 # ENV['CUBESMART_TIMEOUT']
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
26
|
+
## Usage
|
27
|
+
|
28
|
+
```ruby
|
29
|
+
require 'cubesmart'
|
30
|
+
|
31
|
+
sitemap = CubeSmart::Facility.sitemap
|
32
|
+
sitemap.links.each do |link|
|
33
|
+
url = link.loc
|
34
|
+
facility = CubeSmart::Facility.fetch(url:)
|
35
|
+
|
36
|
+
puts facility.text
|
37
|
+
|
38
|
+
facility.prices.each do |price|
|
39
|
+
puts price.text
|
40
|
+
end
|
41
|
+
|
42
|
+
puts
|
43
|
+
end
|
44
|
+
```
|
45
|
+
|
46
|
+
## CLI
|
47
|
+
|
48
|
+
```bash
|
49
|
+
cubesmart crawl
|
50
|
+
```
|
data/bin/console
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'bundler/setup'
|
5
|
+
require 'cubesmart'
|
6
|
+
|
7
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
8
|
+
# with your gem easier. You can also use a different console, if you like.
|
9
|
+
|
10
|
+
require 'irb'
|
11
|
+
IRB.start(__FILE__)
|
data/bin/setup
ADDED
data/exe/cubesmart
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# The address (street + city + state + zip) of a facility.
|
5
|
+
class Address
|
6
|
+
# @attribute [rw] street
|
7
|
+
# @return [String]
|
8
|
+
attr_accessor :street
|
9
|
+
|
10
|
+
# @attribute [rw] city
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :city
|
13
|
+
|
14
|
+
# @attribute [rw] state
|
15
|
+
# @return [String]
|
16
|
+
attr_accessor :state
|
17
|
+
|
18
|
+
# @attribute [rw] zip
|
19
|
+
# @return [String]
|
20
|
+
attr_accessor :zip
|
21
|
+
|
22
|
+
# @param street [String]
|
23
|
+
# @param city [String]
|
24
|
+
# @param state [String]
|
25
|
+
# @param zip [String]
|
26
|
+
def initialize(street:, city:, state:, zip:)
|
27
|
+
@street = street
|
28
|
+
@city = city
|
29
|
+
@state = state
|
30
|
+
@zip = zip
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String]
|
34
|
+
def inspect
|
35
|
+
props = [
|
36
|
+
"street=#{@street.inspect}",
|
37
|
+
"city=#{@city.inspect}",
|
38
|
+
"state=#{@state.inspect}",
|
39
|
+
"zip=#{@zip.inspect}"
|
40
|
+
]
|
41
|
+
"#<#{self.class.name} #{props.join(' ')}>"
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [String]
|
45
|
+
def text
|
46
|
+
"#{street}, #{city}, #{state} #{zip}"
|
47
|
+
end
|
48
|
+
|
49
|
+
# @param data [Hash]
|
50
|
+
#
|
51
|
+
# @return [Address]
|
52
|
+
def self.parse(data:)
|
53
|
+
lines = %w[line1 line2 line3 line4].map { |key| data[key] }
|
54
|
+
|
55
|
+
new(
|
56
|
+
street: lines.compact.reject(&:empty?).join(' '),
|
57
|
+
city: data['city'],
|
58
|
+
state: data['stateName'],
|
59
|
+
zip: data['postalCode']
|
60
|
+
)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'optparse'
|
4
|
+
|
5
|
+
module CubeSmart
|
6
|
+
# Used when interacting with the library from the command line interface (CLI).
|
7
|
+
#
|
8
|
+
# Usage:
|
9
|
+
#
|
10
|
+
# cli = CubeSmart::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
|
+
CubeSmart::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: cubesmart [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
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# The core configuration.
|
5
|
+
class Config
|
6
|
+
# @attribute [rw] user_agent
|
7
|
+
# @return [String]
|
8
|
+
attr_accessor :user_agent
|
9
|
+
|
10
|
+
# @attribute [rw] timeout
|
11
|
+
# @return [Integer]
|
12
|
+
attr_accessor :timeout
|
13
|
+
|
14
|
+
def initialize
|
15
|
+
@user_agent = ENV.fetch('CUBESMART_USER_AGENT', "cubesmart.rb/#{VERSION}")
|
16
|
+
@timeout = Integer(ENV.fetch('CUBESMART_TIMEOUT', 60))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# Used to fetch and parse either HTML or XML via a URL.
|
5
|
+
class Crawler
|
6
|
+
HOST = 'https://www.cubesmart.com'
|
7
|
+
|
8
|
+
# Raised for unexpected HTTP responses.
|
9
|
+
class FetchError < StandardError
|
10
|
+
# @param url [String]
|
11
|
+
# @param response [HTTP::Response]
|
12
|
+
def initialize(url:, response:)
|
13
|
+
super("url=#{url} status=#{response.status.inspect} body=#{String(response.body).inspect}")
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
# @param url [String]
|
18
|
+
# @raise [FetchError]
|
19
|
+
# @return [Nokogiri::HTML::Document]
|
20
|
+
def self.html(url:)
|
21
|
+
new.html(url:)
|
22
|
+
end
|
23
|
+
|
24
|
+
# @param url [String]
|
25
|
+
# @raise [FetchError]
|
26
|
+
# @return [Nokogiri::XML::Document]
|
27
|
+
def self.xml(url:)
|
28
|
+
new.xml(url:)
|
29
|
+
end
|
30
|
+
|
31
|
+
# @return [HTTP::Client]
|
32
|
+
def connection
|
33
|
+
@connection ||= begin
|
34
|
+
config = CubeSmart.config
|
35
|
+
|
36
|
+
connection = HTTP.persistent(HOST)
|
37
|
+
connection = connection.headers('User-Agent' => config.user_agent) if config.user_agent
|
38
|
+
connection = connection.timeout(config.timeout) if config.timeout
|
39
|
+
connection
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# @param url [String]
|
44
|
+
# @return [HTTP::Response]
|
45
|
+
def fetch(url:)
|
46
|
+
response = connection.get(url)
|
47
|
+
raise FetchError.new(url:, response: response.flush) unless response.status.ok?
|
48
|
+
|
49
|
+
response
|
50
|
+
end
|
51
|
+
|
52
|
+
# @param url [String]
|
53
|
+
# @raise [FetchError]
|
54
|
+
# @return [Nokogiri::XML::Document]
|
55
|
+
def html(url:)
|
56
|
+
Nokogiri::HTML(String(fetch(url:).body))
|
57
|
+
end
|
58
|
+
|
59
|
+
# @param url [String]
|
60
|
+
# @raise [FetchError]
|
61
|
+
# @return [Nokogiri::XML::Document]
|
62
|
+
def xml(url:)
|
63
|
+
Nokogiri::XML(String(fetch(url:).body))
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# The dimensions (width + depth + sqft) of a price.
|
5
|
+
class Dimensions
|
6
|
+
# @attribute [rw] depth
|
7
|
+
# @return [Integer]
|
8
|
+
attr_accessor :depth
|
9
|
+
|
10
|
+
# @attribute [rw] width
|
11
|
+
# @return [Integer]
|
12
|
+
attr_accessor :width
|
13
|
+
|
14
|
+
# @attribute [rw] sqft
|
15
|
+
# @return [Integer]
|
16
|
+
attr_accessor :sqft
|
17
|
+
|
18
|
+
# @param depth [Integer]
|
19
|
+
# @param width [Integer]
|
20
|
+
# @param sqft [Integer]
|
21
|
+
def initialize(depth:, width:, sqft:)
|
22
|
+
@depth = depth
|
23
|
+
@width = width
|
24
|
+
@sqft = sqft
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String]
|
28
|
+
def inspect
|
29
|
+
props = [
|
30
|
+
"depth=#{@depth.inspect}",
|
31
|
+
"width=#{@width.inspect}",
|
32
|
+
"sqft=#{@sqft.inspect}"
|
33
|
+
]
|
34
|
+
"#<#{self.class.name} #{props.join(' ')}>"
|
35
|
+
end
|
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
|
+
|
42
|
+
# @param data [Hash]
|
43
|
+
#
|
44
|
+
# @return [Dimensions]
|
45
|
+
def self.parse(data:)
|
46
|
+
new(depth: data['depth'], width: data['width'], sqft: data['squareFoot'])
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,116 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# A facility (address + geocode + prices) on cubesmart.com.
|
5
|
+
#
|
6
|
+
# e.g. https://www.cubesmart.com/arizona-self-storage/chandler-self-storage/2.html
|
7
|
+
class Facility
|
8
|
+
class ParseError < StandardError; end
|
9
|
+
|
10
|
+
SITEMAP_URL = 'https://www.cubesmart.com/sitemap-facility.xml'
|
11
|
+
|
12
|
+
ID_REGEX = /(?<id>\d+)\.html/
|
13
|
+
|
14
|
+
# @attribute [rw] id
|
15
|
+
# @return [String]
|
16
|
+
attr_accessor :id
|
17
|
+
|
18
|
+
# @attribute [rw] name
|
19
|
+
# @return [String]
|
20
|
+
attr_accessor :name
|
21
|
+
|
22
|
+
# @attribute [rw] address
|
23
|
+
# @return [Address]
|
24
|
+
attr_accessor :address
|
25
|
+
|
26
|
+
# @attribute [rw] geocode
|
27
|
+
# @return [Geocode]
|
28
|
+
attr_accessor :geocode
|
29
|
+
|
30
|
+
# @attribute [rw] prices
|
31
|
+
# @return [Array<Price>]
|
32
|
+
attr_accessor :prices
|
33
|
+
|
34
|
+
# @return [Sitemap]
|
35
|
+
def self.sitemap
|
36
|
+
Sitemap.fetch(url: SITEMAP_URL)
|
37
|
+
end
|
38
|
+
|
39
|
+
# @param url [String]
|
40
|
+
#
|
41
|
+
# @return [Facility]
|
42
|
+
def self.fetch(url:)
|
43
|
+
document = Crawler.html(url:)
|
44
|
+
parse(document:)
|
45
|
+
end
|
46
|
+
|
47
|
+
# @param document [Nokogiri::HTML::Document]
|
48
|
+
#
|
49
|
+
# @return [Facility]
|
50
|
+
def self.parse(document:)
|
51
|
+
data = parse_json_ld(document:)
|
52
|
+
|
53
|
+
id = data['url'].match(ID_REGEX)[:id]
|
54
|
+
name = data['name']
|
55
|
+
address = Address.parse(data: data['address'])
|
56
|
+
geocode = Geocode.parse(data: data['geo'])
|
57
|
+
prices = []
|
58
|
+
|
59
|
+
new(id:, name:, address:, geocode:, prices:)
|
60
|
+
end
|
61
|
+
|
62
|
+
# @param document [Nokogiri::HTML::Document]
|
63
|
+
#
|
64
|
+
# @raise [ParseError]
|
65
|
+
#
|
66
|
+
# @return [Hash]
|
67
|
+
def self.parse_json_ld(document:)
|
68
|
+
graph = JSON.parse(document.at_xpath('//script[contains(text(), "@graph")]').text)['@graph']
|
69
|
+
graph.find { |entry| entry['@type'] == 'SelfStorage' } || raise(ParseError, 'missing @graph')
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.crawl
|
73
|
+
sitemap.links.each do |link|
|
74
|
+
url = link.loc
|
75
|
+
|
76
|
+
facility = fetch(url:)
|
77
|
+
puts facility.text
|
78
|
+
|
79
|
+
facility.prices.each do |price|
|
80
|
+
puts price.text
|
81
|
+
end
|
82
|
+
|
83
|
+
puts
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# @param id [String]
|
88
|
+
# @param name [String]
|
89
|
+
# @param address [Address]
|
90
|
+
# @param geocode [Geocode]
|
91
|
+
# @param prices [Array<Price>]
|
92
|
+
def initialize(id:, name:, address:, geocode:, prices:)
|
93
|
+
@id = id
|
94
|
+
@name = name
|
95
|
+
@address = address
|
96
|
+
@geocode = geocode
|
97
|
+
@prices = prices
|
98
|
+
end
|
99
|
+
|
100
|
+
# @return [String]
|
101
|
+
def inspect
|
102
|
+
props = [
|
103
|
+
"id=#{@id.inspect}",
|
104
|
+
"address=#{@address.inspect}",
|
105
|
+
"geocode=#{@geocode.inspect}",
|
106
|
+
"prices=#{@prices.inspect}"
|
107
|
+
]
|
108
|
+
"#<#{self.class.name} #{props.join(' ')}>"
|
109
|
+
end
|
110
|
+
|
111
|
+
# @return [String]
|
112
|
+
def text
|
113
|
+
"#{@id} | #{@name} | #{@address.text} | #{@geocode.text}"
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# The geocode (latitude + longitude) of a facility.
|
5
|
+
class Geocode
|
6
|
+
# @attribute [rw] latitude
|
7
|
+
# @return [Float]
|
8
|
+
attr_accessor :latitude
|
9
|
+
|
10
|
+
# @attribute [rw] longitude
|
11
|
+
# @return [Float]
|
12
|
+
attr_accessor :longitude
|
13
|
+
|
14
|
+
# @param latitude [Float]
|
15
|
+
# @param longitude [Float]
|
16
|
+
def initialize(latitude:, longitude:)
|
17
|
+
@latitude = latitude
|
18
|
+
@longitude = longitude
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
def inspect
|
23
|
+
props = [
|
24
|
+
"latitude=#{@latitude.inspect}",
|
25
|
+
"longitude=#{@longitude.inspect}"
|
26
|
+
]
|
27
|
+
"#<#{self.class.name} #{props.join(' ')}>"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String]
|
31
|
+
def text
|
32
|
+
"#{@latitude},#{@longitude}"
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param data [Hash]
|
36
|
+
#
|
37
|
+
# @return [Geocode]
|
38
|
+
def self.parse(data:)
|
39
|
+
new(
|
40
|
+
latitude: data['latitude'],
|
41
|
+
longitude: data['longitude']
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
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
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# The price (id + dimensions + rate) for a facility
|
5
|
+
class Price
|
6
|
+
# @attribute [rw] id
|
7
|
+
# @return [String]
|
8
|
+
attr_accessor :id
|
9
|
+
|
10
|
+
# @attribute [rw] dimensions
|
11
|
+
# @return [Dimensions]
|
12
|
+
attr_accessor :dimensions
|
13
|
+
|
14
|
+
# @attribute [rw] rates
|
15
|
+
# @return [Rates]
|
16
|
+
attr_accessor :rates
|
17
|
+
|
18
|
+
# @param id [String]
|
19
|
+
# @param dimensions [Dimensions]
|
20
|
+
# @param rates [Rates]
|
21
|
+
def initialize(id:, dimensions:, rates:)
|
22
|
+
@id = id
|
23
|
+
@dimensions = dimensions
|
24
|
+
@rates = rates
|
25
|
+
end
|
26
|
+
|
27
|
+
# @return [String]
|
28
|
+
def inspect
|
29
|
+
props = [
|
30
|
+
"id=#{@id.inspect}",
|
31
|
+
"dimensions=#{@dimensions.inspect}",
|
32
|
+
"rates=#{@rates.inspect}"
|
33
|
+
]
|
34
|
+
"#<#{self.class.name} #{props.join(' ')}>"
|
35
|
+
end
|
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
|
+
|
42
|
+
# @param data [Hash]
|
43
|
+
#
|
44
|
+
# @return [Price]
|
45
|
+
def self.parse(data:)
|
46
|
+
dimensions = Dimensions.parse(data: data['dimensions'])
|
47
|
+
rates = Rates.parse(data: data['rates'])
|
48
|
+
new(
|
49
|
+
id: data['uid'],
|
50
|
+
dimensions: dimensions,
|
51
|
+
rates: rates
|
52
|
+
)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# The rates (street + web) for a facility
|
5
|
+
class Rates
|
6
|
+
# @attribute [rw] street
|
7
|
+
# @return [Integer]
|
8
|
+
attr_accessor :street
|
9
|
+
|
10
|
+
# @attribute [rw] web
|
11
|
+
# @return [Integer]
|
12
|
+
attr_accessor :web
|
13
|
+
|
14
|
+
# @param street [Integer]
|
15
|
+
# @param web [Integer]
|
16
|
+
def initialize(street:, web:)
|
17
|
+
@street = street
|
18
|
+
@web = web
|
19
|
+
end
|
20
|
+
|
21
|
+
# @return [String]
|
22
|
+
def inspect
|
23
|
+
props = [
|
24
|
+
"street=#{@street.inspect}",
|
25
|
+
"web=#{@web.inspect}"
|
26
|
+
]
|
27
|
+
"#<#{self.class.name} #{props.join(' ')}>"
|
28
|
+
end
|
29
|
+
|
30
|
+
# @return [String] e.g. "$80 (street) | $60 (web)"
|
31
|
+
def text
|
32
|
+
"$#{@street} (street) | $#{@web} (web)"
|
33
|
+
end
|
34
|
+
|
35
|
+
# @param data [Hash]
|
36
|
+
#
|
37
|
+
# @return [Rates]
|
38
|
+
def self.parse(data:)
|
39
|
+
new(
|
40
|
+
street: data['street'],
|
41
|
+
web: data['web']
|
42
|
+
)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module CubeSmart
|
4
|
+
# A sitemap on cubesmart.com.
|
5
|
+
#
|
6
|
+
# e.g. https://www.cubesmart.com/sitemap-facility.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
|
data/lib/cubesmart.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'http'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'zeitwerk'
|
6
|
+
|
7
|
+
loader = Zeitwerk::Loader.for_gem
|
8
|
+
loader.inflector.inflect 'cubesmart' => 'CubeSmart'
|
9
|
+
loader.inflector.inflect 'cli' => 'CLI'
|
10
|
+
loader.setup
|
11
|
+
|
12
|
+
# An interface for CubeSmart.
|
13
|
+
module CubeSmart
|
14
|
+
class Error < StandardError; end
|
15
|
+
|
16
|
+
# @return [Config]
|
17
|
+
def self.config
|
18
|
+
@config ||= Config.new
|
19
|
+
end
|
20
|
+
|
21
|
+
# @yield [config]
|
22
|
+
# @yieldparam config [Config]
|
23
|
+
def self.configure
|
24
|
+
yield config
|
25
|
+
end
|
26
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: cubesmart
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Kevin Sylvestre
|
8
|
+
autorequire:
|
9
|
+
bindir: exe
|
10
|
+
cert_chain: []
|
11
|
+
date: 2024-12-03 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: http
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :runtime
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: json
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: nokogiri
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - ">="
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '0'
|
48
|
+
type: :runtime
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
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'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: zeitwerk
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - ">="
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '0'
|
76
|
+
type: :runtime
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - ">="
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '0'
|
83
|
+
description: Uses HTTP.rb to scrape cubesmart.com.
|
84
|
+
email:
|
85
|
+
- kevin@ksylvest.com
|
86
|
+
executables:
|
87
|
+
- cubesmart
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- Gemfile
|
92
|
+
- README.md
|
93
|
+
- bin/console
|
94
|
+
- bin/setup
|
95
|
+
- exe/cubesmart
|
96
|
+
- lib/cubesmart.rb
|
97
|
+
- lib/cubesmart/address.rb
|
98
|
+
- lib/cubesmart/cli.rb
|
99
|
+
- lib/cubesmart/config.rb
|
100
|
+
- lib/cubesmart/crawler.rb
|
101
|
+
- lib/cubesmart/dimensions.rb
|
102
|
+
- lib/cubesmart/facility.rb
|
103
|
+
- lib/cubesmart/geocode.rb
|
104
|
+
- lib/cubesmart/link.rb
|
105
|
+
- lib/cubesmart/price.rb
|
106
|
+
- lib/cubesmart/rates.rb
|
107
|
+
- lib/cubesmart/sitemap.rb
|
108
|
+
- lib/cubesmart/version.rb
|
109
|
+
homepage: https://github.com/ksylvest/cubesmart
|
110
|
+
licenses:
|
111
|
+
- MIT
|
112
|
+
metadata:
|
113
|
+
rubygems_mfa_required: 'true'
|
114
|
+
homepage_uri: https://github.com/ksylvest/cubesmart
|
115
|
+
source_code_uri: https://github.com/ksylvest/cubesmart
|
116
|
+
changelog_uri: https://github.com/ksylvest/cubesmart
|
117
|
+
post_install_message:
|
118
|
+
rdoc_options: []
|
119
|
+
require_paths:
|
120
|
+
- lib
|
121
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
version: 3.2.0
|
126
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
127
|
+
requirements:
|
128
|
+
- - ">="
|
129
|
+
- !ruby/object:Gem::Version
|
130
|
+
version: '0'
|
131
|
+
requirements: []
|
132
|
+
rubygems_version: 3.5.22
|
133
|
+
signing_key:
|
134
|
+
specification_version: 4
|
135
|
+
summary: A crawler for CubeSmart.
|
136
|
+
test_files: []
|