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 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
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
6
+
7
+ gem 'rake'
8
+ gem 'rspec'
9
+ gem 'rspec_junit_formatter'
10
+ gem 'rubocop'
11
+ gem 'rubocop-rake'
12
+ gem 'rubocop-rspec'
13
+ gem 'vcr'
14
+ gem 'webmock'
15
+ gem 'yard'
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # CubeSmart
2
+
3
+ [![LICENSE](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ksylvest/cubesmart/blob/main/LICENSE)
4
+ [![RubyGems](https://img.shields.io/gem/v/cubesmart)](https://rubygems.org/gems/cubesmart)
5
+ [![GitHub](https://img.shields.io/badge/github-repo-blue.svg)](https://github.com/ksylvest/cubesmart)
6
+ [![Yard](https://img.shields.io/badge/docs-site-blue.svg)](https://cubesmart.ksylvest.com)
7
+ [![CircleCI](https://img.shields.io/circleci/build/github/ksylvest/cubesmart)](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
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/cubesmart ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'cubesmart'
5
+
6
+ cli = CubeSmart::CLI.new
7
+ cli.parse
@@ -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
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CubeSmart
4
+ VERSION = '0.1.0'
5
+ 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: []