extraspace 0.3.0 → 0.4.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 +4 -4
- data/README.md +11 -0
- data/lib/extraspace/address.rb +14 -14
- data/lib/extraspace/cli.rb +1 -1
- data/lib/extraspace/config.rb +58 -0
- data/lib/extraspace/crawl.rb +37 -0
- data/lib/extraspace/crawler.rb +17 -1
- data/lib/extraspace/dimensions.rb +31 -19
- data/lib/extraspace/facility.rb +1 -1
- data/lib/extraspace/features.rb +80 -0
- data/lib/extraspace/geocode.rb +10 -10
- data/lib/extraspace/price.rb +20 -14
- data/lib/extraspace/rates.rb +10 -10
- data/lib/extraspace/version.rb +1 -1
- data/lib/extraspace.rb +12 -0
- metadata +5 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 5a9b243e73e7e8ded7dec1cb75998fcf2b6cfc3907e9f07ec1c6a26071c10af4
|
4
|
+
data.tar.gz: 9b56a1dc1d3da8e3d83a358e1554214a58270f0936167004c29063024570445f
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 44636e3f4b7229189e6a806d14bf440fb565127a952a15911e761e440364f83c2413e7167fc55b51ce145eb11f8fe4446c7f259548a5f0dca3adc25a7cb06a4d
|
7
|
+
data.tar.gz: bea8280c5d3506e5358cc69ab1561419667442ca6fc2f78216400bebf60ba2be729fd282e5daee8c79b9dae7dab2f4312fc94d66ed65b5504aeaba67933ebc13
|
data/README.md
CHANGED
@@ -12,6 +12,17 @@
|
|
12
12
|
gem install extrapsace
|
13
13
|
```
|
14
14
|
|
15
|
+
## Configuration
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
require 'extraspace'
|
19
|
+
|
20
|
+
ExtraSpace.configure do |config|
|
21
|
+
config.user_agent = '../..' # ENV['EXTRASPACE_USER_AGENT']
|
22
|
+
config.timeout = 30 # ENV['EXTRASPACE_TIMEOUT']
|
23
|
+
end
|
24
|
+
```
|
25
|
+
|
15
26
|
## Usage
|
16
27
|
|
17
28
|
```ruby
|
data/lib/extraspace/address.rb
CHANGED
@@ -19,6 +19,20 @@ module ExtraSpace
|
|
19
19
|
# @return [String]
|
20
20
|
attr_accessor :zip
|
21
21
|
|
22
|
+
# @param data [Hash]
|
23
|
+
#
|
24
|
+
# @return [Address]
|
25
|
+
def self.parse(data:)
|
26
|
+
lines = %w[line1 line2 line3 line4].map { |key| data[key] }
|
27
|
+
|
28
|
+
new(
|
29
|
+
street: lines.compact.reject(&:empty?).join(' '),
|
30
|
+
city: data['city'],
|
31
|
+
state: data['stateName'],
|
32
|
+
zip: data['postalCode']
|
33
|
+
)
|
34
|
+
end
|
35
|
+
|
22
36
|
# @param street [String]
|
23
37
|
# @param city [String]
|
24
38
|
# @param state [String]
|
@@ -45,19 +59,5 @@ module ExtraSpace
|
|
45
59
|
def text
|
46
60
|
"#{street}, #{city}, #{state} #{zip}"
|
47
61
|
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
62
|
end
|
63
63
|
end
|
data/lib/extraspace/cli.rb
CHANGED
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExtraSpace
|
4
|
+
# The core configuration.
|
5
|
+
class Config
|
6
|
+
# @attribute [rw] accept_language
|
7
|
+
# @return [String]
|
8
|
+
attr_accessor :accept_language
|
9
|
+
|
10
|
+
# @attribute [rw] user_agent
|
11
|
+
# @return [String]
|
12
|
+
attr_accessor :user_agent
|
13
|
+
|
14
|
+
# @attribute [rw] timeout
|
15
|
+
# @return [Integer]
|
16
|
+
attr_accessor :timeout
|
17
|
+
|
18
|
+
# @attribute [rw] proxy_url
|
19
|
+
# @return [String]
|
20
|
+
attr_accessor :proxy_url
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@accept_language = ENV.fetch('EXTRASPACE_ACCEPT_LANGUAGE', 'en-US,en;q=0.9')
|
24
|
+
@user_agent = ENV.fetch('EXTRASPACE_USER_AGENT', "extraspace.rb/#{VERSION}")
|
25
|
+
@timeout = Integer(ENV.fetch('EXTRASPACE_TIMEOUT', 60))
|
26
|
+
@proxy_url = ENV.fetch('EXTRASPACE_PROXY_URL', nil)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @return [Boolean]
|
30
|
+
def headers?
|
31
|
+
!@user_agent.nil?
|
32
|
+
end
|
33
|
+
|
34
|
+
# @return [Boolean]
|
35
|
+
def timeout?
|
36
|
+
!@timeout.zero?
|
37
|
+
end
|
38
|
+
|
39
|
+
# @return [Boolean]
|
40
|
+
def proxy?
|
41
|
+
!@proxy_url.nil?
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Hash<String, String>] e.g { 'User-Agent' => 'extraspace.rb/1.0.0' }
|
45
|
+
def headers
|
46
|
+
{
|
47
|
+
'Accept-Language' => @accept_language,
|
48
|
+
'User-Agent' => @user_agent
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
# @return [Array] e.g. ['proxy.example.com', 8080, 'user', 'pass']
|
53
|
+
def via
|
54
|
+
proxy_uri = URI.parse(@proxy_url)
|
55
|
+
[proxy_uri.host, proxy_uri.port, proxy_uri.user, proxy_uri.password]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExtraSpace
|
4
|
+
# Handles the crawl command via CLI.
|
5
|
+
class Crawl
|
6
|
+
def self.run(...)
|
7
|
+
new(...).run
|
8
|
+
end
|
9
|
+
|
10
|
+
# @param stdout [IO] optional
|
11
|
+
# @param stderr [IO] optional
|
12
|
+
# @param options [Hash] optional
|
13
|
+
def initialize(stdout: $stdout, stderr: $stderr, options: {})
|
14
|
+
@stdout = stdout
|
15
|
+
@stderr = stderr
|
16
|
+
@options = options
|
17
|
+
end
|
18
|
+
|
19
|
+
def run
|
20
|
+
sitemap = Facility.sitemap
|
21
|
+
@stdout.puts("count=#{sitemap.links.count}")
|
22
|
+
@stdout.puts
|
23
|
+
|
24
|
+
sitemap.links.each { |link| process(url: link.loc) }
|
25
|
+
end
|
26
|
+
|
27
|
+
def process(url:)
|
28
|
+
@stdout.puts(url)
|
29
|
+
facility = Facility.fetch(url: url)
|
30
|
+
@stdout.puts(facility.text)
|
31
|
+
facility.prices.each { |price| @stdout.puts(price.text) }
|
32
|
+
@stdout.puts
|
33
|
+
rescue FetchError => e
|
34
|
+
@stderr.puts("url=#{url} error=#{e.message}")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
data/lib/extraspace/crawler.rb
CHANGED
@@ -3,6 +3,8 @@
|
|
3
3
|
module ExtraSpace
|
4
4
|
# Used to fetch and parse either HTML or XML via a URL.
|
5
5
|
class Crawler
|
6
|
+
HOST = 'https://www.extraspace.com'
|
7
|
+
|
6
8
|
# Raised for unexpected HTTP responses.
|
7
9
|
class FetchError < StandardError
|
8
10
|
# @param url [String]
|
@@ -26,10 +28,24 @@ module ExtraSpace
|
|
26
28
|
new.xml(url:)
|
27
29
|
end
|
28
30
|
|
31
|
+
# @return [HTTP::Client]
|
32
|
+
def connection
|
33
|
+
@connection ||= begin
|
34
|
+
config = ExtraSpace.config
|
35
|
+
|
36
|
+
connection = HTTP.use(:auto_deflate).use(:auto_inflate).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 = connection.via(*config.via) if config.proxy?
|
40
|
+
|
41
|
+
connection
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
29
45
|
# @param url [String]
|
30
46
|
# @return [HTTP::Response]
|
31
47
|
def fetch(url:)
|
32
|
-
response =
|
48
|
+
response = connection.get(url)
|
33
49
|
raise FetchError.new(url:, response: response.flush) unless response.status.ok?
|
34
50
|
|
35
51
|
response
|
@@ -3,25 +3,34 @@
|
|
3
3
|
module ExtraSpace
|
4
4
|
# The dimensions (width + depth + sqft) of a price.
|
5
5
|
class Dimensions
|
6
|
+
DEFAULT_HEIGHT = 8.0 # feet
|
7
|
+
|
6
8
|
# @attribute [rw] depth
|
7
|
-
# @return [
|
9
|
+
# @return [Float]
|
8
10
|
attr_accessor :depth
|
9
11
|
|
10
12
|
# @attribute [rw] width
|
11
|
-
# @return [
|
13
|
+
# @return [Float]
|
12
14
|
attr_accessor :width
|
13
15
|
|
14
|
-
# @attribute [rw]
|
15
|
-
# @return [
|
16
|
-
attr_accessor :
|
16
|
+
# @attribute [rw] height
|
17
|
+
# @return [Float]
|
18
|
+
attr_accessor :height
|
17
19
|
|
18
|
-
# @param
|
19
|
-
#
|
20
|
-
# @
|
21
|
-
def
|
20
|
+
# @param data [Hash]
|
21
|
+
#
|
22
|
+
# @return [Dimensions]
|
23
|
+
def self.parse(data:)
|
24
|
+
new(depth: data['depth'], width: data['width'], height: DEFAULT_HEIGHT)
|
25
|
+
end
|
26
|
+
|
27
|
+
# @param depth [Float]
|
28
|
+
# @param width [Float]
|
29
|
+
# @param height [Float]
|
30
|
+
def initialize(depth:, width:, height: DEFAULT_HEIGHT)
|
22
31
|
@depth = depth
|
23
32
|
@width = width
|
24
|
-
@
|
33
|
+
@height = height
|
25
34
|
end
|
26
35
|
|
27
36
|
# @return [String]
|
@@ -29,21 +38,24 @@ module ExtraSpace
|
|
29
38
|
props = [
|
30
39
|
"depth=#{@depth.inspect}",
|
31
40
|
"width=#{@width.inspect}",
|
32
|
-
"
|
41
|
+
"height=#{@height.inspect}"
|
33
42
|
]
|
34
43
|
"#<#{self.class.name} #{props.join(' ')}>"
|
35
44
|
end
|
36
45
|
|
37
|
-
# @return [
|
38
|
-
def
|
39
|
-
|
46
|
+
# @return [Integer]
|
47
|
+
def sqft
|
48
|
+
Integer(@width * @depth)
|
40
49
|
end
|
41
50
|
|
42
|
-
# @
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
51
|
+
# @return [Integer]
|
52
|
+
def cuft
|
53
|
+
Integer(@width * @depth * @height)
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [String] e.g. "10' × 10' (100 sqft)"
|
57
|
+
def text
|
58
|
+
"#{format('%g', @width)}' × #{format('%g', @depth)}' (#{sqft} sqft)"
|
47
59
|
end
|
48
60
|
end
|
49
61
|
end
|
data/lib/extraspace/facility.rb
CHANGED
@@ -0,0 +1,80 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ExtraSpace
|
4
|
+
# The features (e.g. climate-controlled, inside-drive-up-access, outside-drive-up-access, etc) of a price.
|
5
|
+
class Features
|
6
|
+
FIRST_FLOOR_ACCESS_NAME = '1stFloorAccess'
|
7
|
+
CLIMATE_CONTROLLED_NAME = 'ClimateControlled'
|
8
|
+
DRIVE_UP_ACCESS_NAME = 'DriveUpAccess'
|
9
|
+
ELEVATOR_ACCESS_NAME = 'ElevatorAccess'
|
10
|
+
|
11
|
+
# @param data [Array<Hash>]
|
12
|
+
#
|
13
|
+
# @return [Features]
|
14
|
+
def self.parse(data:)
|
15
|
+
new(
|
16
|
+
climate_controlled: data.any? { |feature| feature['name'].eql?(CLIMATE_CONTROLLED_NAME) },
|
17
|
+
drive_up_access: data.any? { |feature| feature['name'].eql?(DRIVE_UP_ACCESS_NAME) },
|
18
|
+
elevator_access: data.any? { |feature| feature['name'].eql?(ELEVATOR_ACCESS_NAME) },
|
19
|
+
first_floor_access: data.any? { |feature| feature['name'].eql?(FIRST_FLOOR_ACCESS_NAME) }
|
20
|
+
)
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param climate_controlled [Boolean]
|
24
|
+
# @param drive_up_access [Boolean]
|
25
|
+
# @param first_floor_access [Boolean]
|
26
|
+
def initialize(climate_controlled:, drive_up_access:, elevator_access:, first_floor_access:)
|
27
|
+
@climate_controlled = climate_controlled
|
28
|
+
@drive_up_access = drive_up_access
|
29
|
+
@elevator_access = elevator_access
|
30
|
+
@first_floor_access = first_floor_access
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [String]
|
34
|
+
def inspect
|
35
|
+
props = [
|
36
|
+
"climate_controlled=#{@climate_controlled}",
|
37
|
+
"drive_up_access=#{@drive_up_access}",
|
38
|
+
"elevator_access=#{@elevator_access}",
|
39
|
+
"first_floor_access=#{@first_floor_access}"
|
40
|
+
]
|
41
|
+
|
42
|
+
"#<#{self.class.name} #{props.join(' ')}>"
|
43
|
+
end
|
44
|
+
|
45
|
+
# @return [String] e.g. "Climate Controlled + First Floor Access"
|
46
|
+
def text
|
47
|
+
amenities.join(' + ')
|
48
|
+
end
|
49
|
+
|
50
|
+
# @return [Array<String>]
|
51
|
+
def amenities
|
52
|
+
[].tap do |amenities|
|
53
|
+
amenities << 'Climate Controlled' if climate_controlled?
|
54
|
+
amenities << 'Drive-Up Access' if drive_up_access?
|
55
|
+
amenities << 'Elevator Access' if elevator_access?
|
56
|
+
amenities << 'First Floor Access' if first_floor_access?
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
# @return [Boolean]
|
61
|
+
def climate_controlled?
|
62
|
+
@climate_controlled
|
63
|
+
end
|
64
|
+
|
65
|
+
# @return [Boolean]
|
66
|
+
def drive_up_access?
|
67
|
+
@drive_up_access
|
68
|
+
end
|
69
|
+
|
70
|
+
# @return [Boolean]
|
71
|
+
def elevator_access?
|
72
|
+
@elevator_access
|
73
|
+
end
|
74
|
+
|
75
|
+
# @return [Boolean]
|
76
|
+
def first_floor_access?
|
77
|
+
@first_floor_access
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
data/lib/extraspace/geocode.rb
CHANGED
@@ -11,6 +11,16 @@ module ExtraSpace
|
|
11
11
|
# @return [Float]
|
12
12
|
attr_accessor :longitude
|
13
13
|
|
14
|
+
# @param data [Hash]
|
15
|
+
#
|
16
|
+
# @return [Geocode]
|
17
|
+
def self.parse(data:)
|
18
|
+
new(
|
19
|
+
latitude: data['latitude'],
|
20
|
+
longitude: data['longitude']
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
14
24
|
# @param latitude [Float]
|
15
25
|
# @param longitude [Float]
|
16
26
|
def initialize(latitude:, longitude:)
|
@@ -31,15 +41,5 @@ module ExtraSpace
|
|
31
41
|
def text
|
32
42
|
"#{@latitude},#{@longitude}"
|
33
43
|
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
44
|
end
|
45
45
|
end
|
data/lib/extraspace/price.rb
CHANGED
@@ -11,16 +11,34 @@ module ExtraSpace
|
|
11
11
|
# @return [Dimensions]
|
12
12
|
attr_accessor :dimensions
|
13
13
|
|
14
|
+
# @attribute [rw] features
|
15
|
+
# @return [Features]
|
16
|
+
attr_accessor :features
|
17
|
+
|
14
18
|
# @attribute [rw] rates
|
15
19
|
# @return [Rates]
|
16
20
|
attr_accessor :rates
|
17
21
|
|
22
|
+
# @param data [Hash]
|
23
|
+
#
|
24
|
+
# @return [Price]
|
25
|
+
def self.parse(data:)
|
26
|
+
new(
|
27
|
+
id: data['uid'],
|
28
|
+
dimensions: Dimensions.parse(data: data['dimensions']),
|
29
|
+
features: Features.parse(data: data['features']),
|
30
|
+
rates: Rates.parse(data: data['rates'])
|
31
|
+
)
|
32
|
+
end
|
33
|
+
|
18
34
|
# @param id [String]
|
19
35
|
# @param dimensions [Dimensions]
|
36
|
+
# @param features [Features]
|
20
37
|
# @param rates [Rates]
|
21
|
-
def initialize(id:, dimensions:, rates:)
|
38
|
+
def initialize(id:, dimensions:, features:, rates:)
|
22
39
|
@id = id
|
23
40
|
@dimensions = dimensions
|
41
|
+
@features = features
|
24
42
|
@rates = rates
|
25
43
|
end
|
26
44
|
|
@@ -29,6 +47,7 @@ module ExtraSpace
|
|
29
47
|
props = [
|
30
48
|
"id=#{@id.inspect}",
|
31
49
|
"dimensions=#{@dimensions.inspect}",
|
50
|
+
"features=#{@features.inspect}",
|
32
51
|
"rates=#{@rates.inspect}"
|
33
52
|
]
|
34
53
|
"#<#{self.class.name} #{props.join(' ')}>"
|
@@ -38,18 +57,5 @@ module ExtraSpace
|
|
38
57
|
def text
|
39
58
|
"#{@id} | #{@dimensions.text} | #{@rates.text}"
|
40
59
|
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
60
|
end
|
55
61
|
end
|
data/lib/extraspace/rates.rb
CHANGED
@@ -11,6 +11,16 @@ module ExtraSpace
|
|
11
11
|
# @return [Integer]
|
12
12
|
attr_accessor :web
|
13
13
|
|
14
|
+
# @param data [Hash]
|
15
|
+
#
|
16
|
+
# @return [Rates]
|
17
|
+
def self.parse(data:)
|
18
|
+
new(
|
19
|
+
street: data['street'],
|
20
|
+
web: data['web']
|
21
|
+
)
|
22
|
+
end
|
23
|
+
|
14
24
|
# @param street [Integer]
|
15
25
|
# @param web [Integer]
|
16
26
|
def initialize(street:, web:)
|
@@ -31,15 +41,5 @@ module ExtraSpace
|
|
31
41
|
def text
|
32
42
|
"$#{@street} (street) | $#{@web} (web)"
|
33
43
|
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
44
|
end
|
45
45
|
end
|
data/lib/extraspace/version.rb
CHANGED
data/lib/extraspace.rb
CHANGED
@@ -9,6 +9,18 @@ loader.inflector.inflect 'extraspace' => 'ExtraSpace'
|
|
9
9
|
loader.inflector.inflect 'cli' => 'CLI'
|
10
10
|
loader.setup
|
11
11
|
|
12
|
+
# An interface for ExtraSpace.
|
12
13
|
module ExtraSpace
|
13
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
|
14
26
|
end
|
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.
|
4
|
+
version: 0.4.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
|
+
date: 2024-12-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: http
|
@@ -96,9 +96,12 @@ files:
|
|
96
96
|
- lib/extraspace.rb
|
97
97
|
- lib/extraspace/address.rb
|
98
98
|
- lib/extraspace/cli.rb
|
99
|
+
- lib/extraspace/config.rb
|
100
|
+
- lib/extraspace/crawl.rb
|
99
101
|
- lib/extraspace/crawler.rb
|
100
102
|
- lib/extraspace/dimensions.rb
|
101
103
|
- lib/extraspace/facility.rb
|
104
|
+
- lib/extraspace/features.rb
|
102
105
|
- lib/extraspace/geocode.rb
|
103
106
|
- lib/extraspace/link.rb
|
104
107
|
- lib/extraspace/price.rb
|