cubesmart 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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: []