fastly 0.5
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.
- data/.gitignore +6 -0
- data/Gemfile +4 -0
- data/README.md +59 -0
- data/Rakefile +1 -0
- data/bin/fastly_upload_vcl +67 -0
- data/fastly.gemspec +23 -0
- data/lib/fastly.rb +357 -0
- data/lib/fastly/backend.rb +99 -0
- data/lib/fastly/base.rb +62 -0
- data/lib/fastly/belongs_to_service_and_version.rb +42 -0
- data/lib/fastly/client.rb +160 -0
- data/lib/fastly/customer.rb +26 -0
- data/lib/fastly/director.rb +47 -0
- data/lib/fastly/domain.rb +29 -0
- data/lib/fastly/fetcher.rb +56 -0
- data/lib/fastly/invoice.rb +95 -0
- data/lib/fastly/match.rb +77 -0
- data/lib/fastly/origin.rb +23 -0
- data/lib/fastly/service.rb +112 -0
- data/lib/fastly/settings.rb +69 -0
- data/lib/fastly/user.rb +62 -0
- data/lib/fastly/vcl.rb +28 -0
- data/lib/fastly/version.rb +148 -0
- data/test/admin_test.rb +41 -0
- data/test/api_key_test.rb +52 -0
- data/test/common.rb +155 -0
- data/test/full_login_test.rb +98 -0
- data/test/helper.rb +24 -0
- metadata +157 -0
data/lib/fastly/vcl.rb
ADDED
@@ -0,0 +1,28 @@
|
|
1
|
+
class Fastly
|
2
|
+
# An internal representation of a Varnish Configuration Language file
|
3
|
+
class VCL < BelongsToServiceAndVersion
|
4
|
+
attr_accessor :service_id, :name, :content, :comment
|
5
|
+
##
|
6
|
+
# :attr: service_id
|
7
|
+
#
|
8
|
+
# The id of the service this belongs to.
|
9
|
+
#
|
10
|
+
|
11
|
+
##
|
12
|
+
# :attr: version
|
13
|
+
#
|
14
|
+
# The number of the version this belongs to.
|
15
|
+
#
|
16
|
+
|
17
|
+
##
|
18
|
+
# :attr: content
|
19
|
+
#
|
20
|
+
# The content of this VCL
|
21
|
+
#
|
22
|
+
|
23
|
+
##
|
24
|
+
# :attr: comment
|
25
|
+
#
|
26
|
+
# a free form comment field
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,148 @@
|
|
1
|
+
class Fastly
|
2
|
+
# An iteration of your configuration
|
3
|
+
class Version < Base
|
4
|
+
attr_accessor :service_id, :number, :name, :active, :locked, :staging, :testing, :deployed, :comment
|
5
|
+
|
6
|
+
##
|
7
|
+
# :attr: service_id
|
8
|
+
# The id of the service this belongs to.
|
9
|
+
|
10
|
+
##
|
11
|
+
# :attr: number
|
12
|
+
# The generation of this version
|
13
|
+
|
14
|
+
##
|
15
|
+
# :attr: name
|
16
|
+
#
|
17
|
+
# The name of this version.
|
18
|
+
|
19
|
+
|
20
|
+
##
|
21
|
+
# :attr: active
|
22
|
+
#
|
23
|
+
# Whether this version is active or not.
|
24
|
+
|
25
|
+
|
26
|
+
##
|
27
|
+
# :attr: locked
|
28
|
+
#
|
29
|
+
# Whether this version is locked or not.
|
30
|
+
|
31
|
+
|
32
|
+
##
|
33
|
+
# :attr: staging
|
34
|
+
#
|
35
|
+
# Whether this version is in staging or not.
|
36
|
+
|
37
|
+
|
38
|
+
##
|
39
|
+
# :attr: testing
|
40
|
+
#
|
41
|
+
# Whether this version is in testing or not.
|
42
|
+
#
|
43
|
+
|
44
|
+
##
|
45
|
+
# :attr: deployed
|
46
|
+
#
|
47
|
+
# Whether this version is deployed or not.
|
48
|
+
#
|
49
|
+
|
50
|
+
##
|
51
|
+
# :attr: comment
|
52
|
+
#
|
53
|
+
# a free form comment field
|
54
|
+
|
55
|
+
|
56
|
+
# Get the Service object this Version belongs to
|
57
|
+
def service
|
58
|
+
fetcher.get(Fastly::Service, service_id)
|
59
|
+
end
|
60
|
+
|
61
|
+
# Get the Settings object for this Version
|
62
|
+
def settings
|
63
|
+
fetcher.get_settings(service_id, number)
|
64
|
+
end
|
65
|
+
|
66
|
+
# Activate this version
|
67
|
+
def activate!
|
68
|
+
raise Fastly::FullAuthRequired unless fetcher.fully_authed?
|
69
|
+
hash = fetcher.client.put(Fastly::Version.put_path(self)+"/activate")
|
70
|
+
return !hash.nil?
|
71
|
+
end
|
72
|
+
|
73
|
+
# XXX Not currently
|
74
|
+
# def deactivate!
|
75
|
+
# raise Fastly::FullAuthRequired unless fetcher.fully_authed?
|
76
|
+
# hash = fetcher.client.put(Fastly::Version.put_path(self)+"/deactivate")
|
77
|
+
# return !hash.nil?
|
78
|
+
# end
|
79
|
+
|
80
|
+
# Clone this Version
|
81
|
+
def clone
|
82
|
+
raise Fastly::FullAuthRequired unless fetcher.fully_authed?
|
83
|
+
hash = fetcher.client.put(Fastly::Version.put_path(self)+"/clone")
|
84
|
+
return nil if hash.nil?
|
85
|
+
return Fastly::Version.new(hash, fetcher)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Get the generated VCL object for this Version (which must have been activated first)
|
89
|
+
#
|
90
|
+
# Won't return the content of the VCL unless you pass in
|
91
|
+
# :include_content => true
|
92
|
+
# in the opts
|
93
|
+
def generated_vcl(opts={})
|
94
|
+
raise Fastly::FullAuthRequired unless fetcher.fully_authed?
|
95
|
+
hash = fetcher.client.get(Fastly::Version.put_path(self)+"/generated_vcl", opts)
|
96
|
+
opts = {
|
97
|
+
'content' => hash['vcl'] || hash['content'],
|
98
|
+
'name' => hash['md5'],
|
99
|
+
'version' => hash['version'],
|
100
|
+
'service_id' => hash['service']
|
101
|
+
}
|
102
|
+
return Fastly::VCL.new(opts, fetcher)
|
103
|
+
end
|
104
|
+
|
105
|
+
# Upload a VCL file for this Version
|
106
|
+
def upload_vcl(name, content)
|
107
|
+
raise Fastly::FullAuthRequired unless fetcher.fully_authed?
|
108
|
+
hash = fetcher.client.post(Fastly::Version.put_path(self)+"/vcl", :name => name, :content => content)
|
109
|
+
return nil if hash.nil?
|
110
|
+
return Fastly::VCL.new(hash, fetcher)
|
111
|
+
end
|
112
|
+
|
113
|
+
# Get the named VCL for this version
|
114
|
+
#
|
115
|
+
# Won't return the content of the VCL unless you pass in
|
116
|
+
# :include_content => true
|
117
|
+
# in the opts
|
118
|
+
def vcl(name, opts={})
|
119
|
+
raise Fastly::FullAuthRequired unless fetcher.fully_authed?
|
120
|
+
fetcher.get_vcl(service_id, number, name, opts)
|
121
|
+
end
|
122
|
+
|
123
|
+
# Validate this Version
|
124
|
+
def validate
|
125
|
+
raise Fastly::FullAuthRequired unless fetcher.fully_authed?
|
126
|
+
hash = fetcher.client.get(Fastly::Version.put_path(self)+"/validate")
|
127
|
+
return !hash.nil?
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def self.get_path(service, number)
|
133
|
+
"/service/#{service}/version/#{number}"
|
134
|
+
end
|
135
|
+
|
136
|
+
def self.post_path(opts)
|
137
|
+
"/service/#{opts[:service_id]}/version"
|
138
|
+
end
|
139
|
+
|
140
|
+
def self.put_path(obj)
|
141
|
+
get_path(obj.service_id, obj.number)
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.delete_path(obj)
|
145
|
+
put_path(obj)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
end
|
data/test/admin_test.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'test/unit'
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/helper')
|
6
|
+
|
7
|
+
class AdminTest < Test::Unit::TestCase
|
8
|
+
def setup
|
9
|
+
opts = login_opts(:full).merge(:use_curb => false)
|
10
|
+
begin
|
11
|
+
@client = Fastly::Client.new(opts)
|
12
|
+
@fastly = Fastly.new(opts)
|
13
|
+
rescue Exception => e
|
14
|
+
warn e.inspect
|
15
|
+
warn e.backtrace.join("\n")
|
16
|
+
exit(-1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_creating_and_updating_customer
|
21
|
+
return unless @fastly.current_user.can_do?(:admin)
|
22
|
+
customer = @fastly.create_customer(:name => "fastly-ruby-test-customer-#{get_rand}")
|
23
|
+
email = "fastly-ruby-test-#{get_rand}-new@example.com"
|
24
|
+
user = @fastly.create_user(:login => email, :name => "New User")
|
25
|
+
customer.owner_id = user.id
|
26
|
+
|
27
|
+
tmp = @fastly.update_customer(customer)
|
28
|
+
assert tmp
|
29
|
+
assert_equal customer.id, tmp.id
|
30
|
+
assert_equal customer.owner.id, tmp.owner.id
|
31
|
+
end
|
32
|
+
|
33
|
+
def test_creating_and_updating_customer_with_owner
|
34
|
+
return unless @fastly.current_user.can_do?(:admin)
|
35
|
+
email = "fastly-ruby-test-#{get_rand}-new@example.com"
|
36
|
+
customer = @fastly.create_customer(:name => "fastly-ruby-test-customer-#{get_rand}", :owner => { :login => email, :name => "Test NewOwner" })
|
37
|
+
assert customer
|
38
|
+
assert_equal customer.owner.login, email
|
39
|
+
end
|
40
|
+
|
41
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'test/unit'
|
4
|
+
require File.expand_path(File.dirname(__FILE__) + '/helper')
|
5
|
+
|
6
|
+
class ApiKeyTest < Test::Unit::TestCase
|
7
|
+
include CommonTests
|
8
|
+
|
9
|
+
def setup
|
10
|
+
@opts = login_opts(:api_key)
|
11
|
+
begin
|
12
|
+
@client = Fastly::Client.new(@opts)
|
13
|
+
@fastly = Fastly.new(@opts)
|
14
|
+
rescue Exception => e
|
15
|
+
pp e
|
16
|
+
exit(-1)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def test_raw_client
|
21
|
+
user = customer = nil
|
22
|
+
assert_raise(Fastly::Error) {
|
23
|
+
user = @client.get('/current_user')
|
24
|
+
}
|
25
|
+
assert_equal nil, user
|
26
|
+
assert_raise(Fastly::Error) {
|
27
|
+
customer = @client.get('/current_customer')
|
28
|
+
}
|
29
|
+
assert_equal nil, customer
|
30
|
+
end
|
31
|
+
|
32
|
+
|
33
|
+
def test_current_user_and_customer
|
34
|
+
current_user = current_customer = nil
|
35
|
+
assert_raise(Fastly::Error) {
|
36
|
+
current_user = @fastly.current_user
|
37
|
+
}
|
38
|
+
assert_equal nil, current_user
|
39
|
+
|
40
|
+
assert_raise(Fastly::Error) {
|
41
|
+
current_customer = @fastly.current_customer
|
42
|
+
}
|
43
|
+
assert_equal nil, current_customer
|
44
|
+
end
|
45
|
+
|
46
|
+
|
47
|
+
def test_purging
|
48
|
+
#assert @fastly.purge('foo')
|
49
|
+
# TODO Won't work until we get fixtures in Heavenly
|
50
|
+
#assert @fastly.purge_all('foo')
|
51
|
+
end
|
52
|
+
end
|
data/test/common.rb
ADDED
@@ -0,0 +1,155 @@
|
|
1
|
+
module CommonTests
|
2
|
+
def test_creating_service_and_backend
|
3
|
+
name = "fastly-test-service-#{get_rand}"
|
4
|
+
service = @fastly.create_service(:name => name)
|
5
|
+
assert service
|
6
|
+
assert_equal name, service.name
|
7
|
+
tmp = @fastly.get_service(service.id)
|
8
|
+
assert tmp
|
9
|
+
assert_equal name, tmp.name
|
10
|
+
|
11
|
+
version = service.version
|
12
|
+
assert version
|
13
|
+
|
14
|
+
settings = @fastly.get_settings(service.id, version.number)
|
15
|
+
assert settings
|
16
|
+
assert_equal settings.service_id, service.id
|
17
|
+
assert_equal settings.version, version.number
|
18
|
+
|
19
|
+
default_ttl = settings.settings['general.default_ttl']
|
20
|
+
settings = version.settings
|
21
|
+
assert settings
|
22
|
+
assert_equal settings.service_id, service.id
|
23
|
+
assert_equal settings.version, version.number
|
24
|
+
assert_equal settings.settings['general.default_ttl'], default_ttl
|
25
|
+
|
26
|
+
settings.settings['general.default_ttl'] = default_ttl = "888888888"
|
27
|
+
settings.save!;
|
28
|
+
|
29
|
+
settings = version.settings
|
30
|
+
assert_equal settings.settings['general.default_ttl'], default_ttl;
|
31
|
+
|
32
|
+
services = @fastly.list_services
|
33
|
+
assert !services.empty?
|
34
|
+
assert !services.select { |s| s.name == name }.empty?
|
35
|
+
|
36
|
+
service = @fastly.search_services( :name => name )
|
37
|
+
assert service
|
38
|
+
assert name, service.name
|
39
|
+
|
40
|
+
|
41
|
+
service = @fastly.search_services( :name => name, :version => version.number )
|
42
|
+
assert services
|
43
|
+
assert name, service.name
|
44
|
+
|
45
|
+
version2 = @fastly.create_version(:service_id => service.id)
|
46
|
+
assert version2
|
47
|
+
assert_equal = version.number.to_i+1, version2.number.to_i
|
48
|
+
|
49
|
+
version3 = version2.clone
|
50
|
+
assert version3
|
51
|
+
assert_equal = version2.number.to_i+1, version3.number.to_i
|
52
|
+
|
53
|
+
number = version3.number.to_i
|
54
|
+
|
55
|
+
backend_name = "fastly-test-backend-#{get_rand}"
|
56
|
+
backend = begin
|
57
|
+
@fastly.create_backend(:service_id => service.id, :version => number, :hostname => 'localhost', :name => backend_name)
|
58
|
+
rescue Fastly::Error
|
59
|
+
end
|
60
|
+
assert_nil backend
|
61
|
+
|
62
|
+
backend = @fastly.create_backend(:service_id => service.id, :version => number, :address => '74.125.224.146', :name => backend_name)
|
63
|
+
assert backend
|
64
|
+
assert_equal backend.service_id, service.id
|
65
|
+
assert_equal backend.ipv4, '74.125.224.146'
|
66
|
+
assert_equal backend.address, '74.125.224.146'
|
67
|
+
assert_equal backend.port, '80'
|
68
|
+
|
69
|
+
backend.hostname = 'thegestalt.org'
|
70
|
+
backend.port = '9092'
|
71
|
+
@fastly.update_backend(backend)
|
72
|
+
backend = @fastly.get_backend(service.id, number, backend_name)
|
73
|
+
|
74
|
+
assert backend
|
75
|
+
assert_equal backend.address, 'thegestalt.org'
|
76
|
+
assert_equal backend.hostname, 'thegestalt.org'
|
77
|
+
assert_equal backend.port, '9092'
|
78
|
+
|
79
|
+
|
80
|
+
domain_name = "fastly-test-domain-#{get_rand}-example.com"
|
81
|
+
domain = @fastly.create_domain(:service_id => service.id, :version => number, :name => domain_name)
|
82
|
+
assert domain
|
83
|
+
assert_equal domain_name, domain.name
|
84
|
+
assert_equal domain.service.id, service.id
|
85
|
+
assert_equal domain.version_number.to_s, number.to_s
|
86
|
+
assert_equal domain.version.number.to_s, number.to_s
|
87
|
+
|
88
|
+
domain.comment = "Flibbety gibbet"
|
89
|
+
domain.save!
|
90
|
+
domain = @fastly.get_domain(service.id, number, domain_name)
|
91
|
+
assert_equal domain.name, domain_name
|
92
|
+
assert_equal domain.comment, "Flibbety gibbet"
|
93
|
+
|
94
|
+
director_name = "fastly-test-director-#{get_rand}"
|
95
|
+
director = @fastly.create_director(:service_id => service.id, :version => number, :name => director_name)
|
96
|
+
assert director
|
97
|
+
assert_equal director_name, director.name
|
98
|
+
assert_equal director.service.id, service.id
|
99
|
+
assert_equal director.version_number.to_s, number.to_s
|
100
|
+
assert_equal director.version.number.to_s, number.to_s
|
101
|
+
|
102
|
+
origin_name = "fastly-test-origin-#{get_rand}"
|
103
|
+
origin = @fastly.create_origin(:service_id => service.id, :version => number, :name => origin_name)
|
104
|
+
assert origin
|
105
|
+
assert_equal origin_name, origin.name
|
106
|
+
assert_equal origin.service.id, service.id
|
107
|
+
assert_equal origin.version_number.to_s, number.to_s
|
108
|
+
# assert_equal origin.version.number.to_s, number.to_s
|
109
|
+
|
110
|
+
assert version3.activate!
|
111
|
+
|
112
|
+
generated = version3.generated_vcl
|
113
|
+
assert generated
|
114
|
+
assert generated.content.nil?
|
115
|
+
generated = version3.generated_vcl(:include_content => true)
|
116
|
+
assert !generated.content.nil?
|
117
|
+
assert generated.content.match(/\.port = "9092"/ms)
|
118
|
+
|
119
|
+
assert version3.validate
|
120
|
+
|
121
|
+
#assert @fastly.deactivate_version(version2)
|
122
|
+
end
|
123
|
+
|
124
|
+
def test_stats
|
125
|
+
name = "fastly-test-service-#{get_rand}"
|
126
|
+
service = @fastly.create_service(:name => name)
|
127
|
+
assert service
|
128
|
+
assert_equal name, service.name
|
129
|
+
tmp = @fastly.get_service(service.id)
|
130
|
+
assert tmp
|
131
|
+
assert_equal name, tmp.name
|
132
|
+
|
133
|
+
stats = service.stats
|
134
|
+
assert stats
|
135
|
+
end
|
136
|
+
|
137
|
+
def test_invoices
|
138
|
+
name = "fastly-test-service-#{get_rand}"
|
139
|
+
service = @fastly.create_service(:name => name)
|
140
|
+
assert service
|
141
|
+
assert_equal name, service.name
|
142
|
+
|
143
|
+
invoice = service.invoice
|
144
|
+
assert invoice
|
145
|
+
assert invoice.regions
|
146
|
+
assert_equal invoice.service_id, service.id
|
147
|
+
|
148
|
+
invoices = @fastly.list_invoices
|
149
|
+
services = @fastly.list_services
|
150
|
+
assert_equal invoices.size, services.size
|
151
|
+
assert_equal Fastly::Invoice, invoices[0].class
|
152
|
+
assert invoices[0].service_id
|
153
|
+
end
|
154
|
+
|
155
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require 'rubygems'
|
4
|
+
require 'test/unit'
|
5
|
+
require File.expand_path(File.dirname(__FILE__) + '/helper')
|
6
|
+
|
7
|
+
class FullLoginTest < Test::Unit::TestCase
|
8
|
+
include CommonTests
|
9
|
+
|
10
|
+
def setup
|
11
|
+
@opts = login_opts(:full).merge(:use_curb => false)
|
12
|
+
begin
|
13
|
+
@client = Fastly::Client.new(@opts)
|
14
|
+
@fastly = Fastly.new(@opts)
|
15
|
+
rescue Exception => e
|
16
|
+
warn e.inspect
|
17
|
+
warn e.backtrace.join("\n")
|
18
|
+
exit(-1)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def test_raw_client
|
23
|
+
user = @client.get('/current_user')
|
24
|
+
assert user
|
25
|
+
assert_equal @opts[:user], user['login']
|
26
|
+
assert_equal @opts[:name], user['name']
|
27
|
+
|
28
|
+
customer = @client.get('/current_customer')
|
29
|
+
assert customer
|
30
|
+
assert_equal @opts[:customer], customer['name']
|
31
|
+
end
|
32
|
+
|
33
|
+
|
34
|
+
def test_current_user_and_customer
|
35
|
+
user = @fastly.current_user
|
36
|
+
assert user
|
37
|
+
assert_equal @opts[:user], user.login
|
38
|
+
assert_equal @opts[:name], user.name
|
39
|
+
|
40
|
+
customer = @fastly.current_customer
|
41
|
+
assert customer
|
42
|
+
assert_equal @opts[:customer], customer.name
|
43
|
+
|
44
|
+
tmp_customer = user.customer
|
45
|
+
assert_equal customer.id, tmp_customer.id
|
46
|
+
|
47
|
+
tmp_user = customer.owner
|
48
|
+
assert_equal user.id, tmp_user.id
|
49
|
+
end
|
50
|
+
|
51
|
+
|
52
|
+
def test_fetching_particular_user
|
53
|
+
current_user = @fastly.current_user
|
54
|
+
assert current_user
|
55
|
+
|
56
|
+
id_user = @fastly.get_user(current_user.id)
|
57
|
+
assert_equal current_user.id, id_user.id
|
58
|
+
assert_equal current_user.name, id_user.name
|
59
|
+
|
60
|
+
# FIXME looking up by login doesn't work yet
|
61
|
+
#login_user = @fastly.get_user(current_user.login)
|
62
|
+
#assert_equal current_user.id, login_user.id
|
63
|
+
#assert_equal current_user.name, login_user.name
|
64
|
+
|
65
|
+
current_customer = @fastly.current_customer
|
66
|
+
assert current_customer
|
67
|
+
|
68
|
+
customer = @fastly.get_customer(current_customer.id)
|
69
|
+
assert_equal current_customer.id, customer.id
|
70
|
+
assert_equal current_customer.name, customer.name
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_creating_and_updating_user
|
74
|
+
customer = @fastly.current_customer
|
75
|
+
email = "fastly-ruby-test-#{get_rand}-new@example.com"
|
76
|
+
user = @fastly.create_user(:login => email, :name => "New User")
|
77
|
+
assert user
|
78
|
+
assert_equal customer.id, user.customer_id
|
79
|
+
assert_equal "New User", user.name
|
80
|
+
assert_equal email, user.login
|
81
|
+
|
82
|
+
tmp = @fastly.get_user(user.id)
|
83
|
+
|
84
|
+
assert tmp
|
85
|
+
assert_equal user.id, tmp.id
|
86
|
+
assert_equal user.name, tmp.name
|
87
|
+
|
88
|
+
user.name = "Updated Name"
|
89
|
+
tmp = @fastly.update_user(user)
|
90
|
+
assert tmp
|
91
|
+
assert_equal user.id, tmp.id
|
92
|
+
assert_equal "Updated Name", tmp.name
|
93
|
+
|
94
|
+
assert @fastly.delete_user(user)
|
95
|
+
tmp = @fastly.get_user(user.id)
|
96
|
+
assert_equal nil, tmp
|
97
|
+
end
|
98
|
+
end
|