leadlight 0.0.2
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/Gemfile +9 -0
- data/Gemfile.lock +79 -0
- data/Guardfile +19 -0
- data/Rakefile +133 -0
- data/leadlight.gemspec +125 -0
- data/lib/leadlight.rb +92 -0
- data/lib/leadlight/blank.rb +15 -0
- data/lib/leadlight/codec.rb +63 -0
- data/lib/leadlight/enumerable_representation.rb +24 -0
- data/lib/leadlight/errors.rb +14 -0
- data/lib/leadlight/hyperlinkable.rb +126 -0
- data/lib/leadlight/link.rb +35 -0
- data/lib/leadlight/link_template.rb +47 -0
- data/lib/leadlight/representation.rb +30 -0
- data/lib/leadlight/request.rb +91 -0
- data/lib/leadlight/service.rb +73 -0
- data/lib/leadlight/service_middleware.rb +50 -0
- data/lib/leadlight/tint.rb +26 -0
- data/lib/leadlight/tint_helper.rb +67 -0
- data/lib/leadlight/type.rb +71 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/has_the_expected_content.yml +75 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/_user/indicates_the_expected_oath_scopes.yml +75 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members.yml +384 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/adding_and_removing_team_members/.yml +309 -0
- data/spec/cassettes/Leadlight/authorized_GitHub_example/test_team/.yml +159 -0
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/.yml +32 -0
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/__location__/.yml +32 -0
- data/spec/cassettes/Leadlight/basic_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/__location__/.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_root/should_be_a_204_no_content.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/_user/has_the_expected_content.yml +65 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/.yml +100 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_able_to_follow_next_link.yml +135 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable.yml +275 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_be_enumerable_over_page_boundaries.yml +170 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_followers/should_have_next_and_last_links.yml +100 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/exists.yml +32 -0
- data/spec/cassettes/Leadlight/tinted_GitHub_example/user_link/links_to_the_expected_URL.yml +32 -0
- data/spec/leadlight/codec_spec.rb +93 -0
- data/spec/leadlight/hyperlinkable_spec.rb +136 -0
- data/spec/leadlight/link_spec.rb +53 -0
- data/spec/leadlight/link_template_spec.rb +45 -0
- data/spec/leadlight/representation_spec.rb +62 -0
- data/spec/leadlight/request_spec.rb +236 -0
- data/spec/leadlight/service_middleware_spec.rb +81 -0
- data/spec/leadlight/service_spec.rb +127 -0
- data/spec/leadlight/tint_helper_spec.rb +132 -0
- data/spec/leadlight/type_spec.rb +137 -0
- data/spec/leadlight_spec.rb +237 -0
- data/spec/spec_helper_lite.rb +6 -0
- data/spec/support/credentials.rb +16 -0
- data/spec/support/vcr.rb +18 -0
- metadata +229 -0
@@ -0,0 +1,32 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: https://api.github.com/
|
6
|
+
body: ""
|
7
|
+
headers:
|
8
|
+
Accept:
|
9
|
+
- application/json, text/x-yaml, application/xml, application/xhtml+xml, text/html, text/plain
|
10
|
+
response:
|
11
|
+
status:
|
12
|
+
code: 204
|
13
|
+
message:
|
14
|
+
headers:
|
15
|
+
server:
|
16
|
+
- nginx/1.0.4
|
17
|
+
date:
|
18
|
+
- Mon, 09 Jan 2012 02:31:45 GMT
|
19
|
+
connection:
|
20
|
+
- close
|
21
|
+
status:
|
22
|
+
- 204 No Content
|
23
|
+
x-ratelimit-limit:
|
24
|
+
- "5000"
|
25
|
+
etag:
|
26
|
+
- "\"d41d8cd98f00b204e9800998ecf8427e\""
|
27
|
+
x-ratelimit-remaining:
|
28
|
+
- "4993"
|
29
|
+
body: ""
|
30
|
+
http_version:
|
31
|
+
recorded_at: Mon, 09 Jan 2012 02:31:43 GMT
|
32
|
+
recorded_with: VCR 2.0.0.rc1
|
@@ -0,0 +1,32 @@
|
|
1
|
+
---
|
2
|
+
http_interactions:
|
3
|
+
- request:
|
4
|
+
method: get
|
5
|
+
uri: https://api.github.com/
|
6
|
+
body: ""
|
7
|
+
headers:
|
8
|
+
Accept:
|
9
|
+
- application/json, text/x-yaml, application/xml, application/xhtml+xml, text/html, text/plain
|
10
|
+
response:
|
11
|
+
status:
|
12
|
+
code: 204
|
13
|
+
message:
|
14
|
+
headers:
|
15
|
+
server:
|
16
|
+
- nginx/1.0.4
|
17
|
+
date:
|
18
|
+
- Mon, 09 Jan 2012 02:31:45 GMT
|
19
|
+
connection:
|
20
|
+
- close
|
21
|
+
status:
|
22
|
+
- 204 No Content
|
23
|
+
x-ratelimit-limit:
|
24
|
+
- "5000"
|
25
|
+
etag:
|
26
|
+
- "\"d41d8cd98f00b204e9800998ecf8427e\""
|
27
|
+
x-ratelimit-remaining:
|
28
|
+
- "4992"
|
29
|
+
body: ""
|
30
|
+
http_version:
|
31
|
+
recorded_at: Mon, 09 Jan 2012 02:31:44 GMT
|
32
|
+
recorded_with: VCR 2.0.0.rc1
|
@@ -0,0 +1,93 @@
|
|
1
|
+
require 'spec_helper_lite'
|
2
|
+
require 'leadlight/codec'
|
3
|
+
|
4
|
+
module Leadlight
|
5
|
+
describe Codec do
|
6
|
+
let(:subject) {
|
7
|
+
# Due to the static init in Codec, we need to reload the class
|
8
|
+
# after stubs/mocks are set up
|
9
|
+
load 'leadlight/codec.rb'
|
10
|
+
Codec.new
|
11
|
+
}
|
12
|
+
let(:representation) { stub(:representation) }
|
13
|
+
let(:entity_body) { stub(:entity_body) }
|
14
|
+
|
15
|
+
it 'decodes JSON' do
|
16
|
+
MultiJson.should_receive(:decode).
|
17
|
+
with(entity_body, anything).
|
18
|
+
and_return(representation)
|
19
|
+
subject.decode('application/json', entity_body).
|
20
|
+
should equal(representation)
|
21
|
+
end
|
22
|
+
|
23
|
+
it 'decodes types ending in +json as JSON' do
|
24
|
+
MultiJson.should_receive(:decode).
|
25
|
+
with(entity_body, anything).
|
26
|
+
and_return(representation)
|
27
|
+
subject.decode('application/vnc.example.user+json foo=bar', entity_body).
|
28
|
+
should equal(representation)
|
29
|
+
end
|
30
|
+
|
31
|
+
it 'decodes text/plain as a string' do
|
32
|
+
subject.decode('text/plain', entity_body).should eq(entity_body)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'barfs on unrecognized content-type' do
|
36
|
+
expect{ subject.decode('xyzzy', entity_body) }.
|
37
|
+
to raise_error(ArgumentError)
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'ignores junk after the content type' do
|
41
|
+
subject.decode('text/plain foo=bar', entity_body).should eq(entity_body)
|
42
|
+
end
|
43
|
+
|
44
|
+
it 'ignores whitespace in the content type' do
|
45
|
+
subject.decode(' text/plain foo=bar', entity_body).should eq(entity_body)
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'passes options to the JSON decoder' do
|
49
|
+
MultiJson.should_receive(:decode).
|
50
|
+
with(anything, {foo: 42})
|
51
|
+
subject.decode('application/json', entity_body, foo: 42)
|
52
|
+
end
|
53
|
+
|
54
|
+
it 'decodes JSON with multi_json' do
|
55
|
+
MultiJson.should_receive(:decode).
|
56
|
+
with(entity_body, anything).
|
57
|
+
and_return(representation)
|
58
|
+
subject.decode('application/json', entity_body).
|
59
|
+
should equal(representation)
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'encodes types ending in +json as JSON' do
|
63
|
+
MultiJson.should_receive(:encode).
|
64
|
+
with(representation, anything).
|
65
|
+
and_return(entity_body)
|
66
|
+
subject.encode('application/vnc.example.user+json foo=bar', representation).
|
67
|
+
should equal(entity_body)
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'encodes text/plain as a string' do
|
71
|
+
subject.encode('text/plain', representation).should eq(representation.to_s)
|
72
|
+
end
|
73
|
+
|
74
|
+
it 'barfs on encoding unrecognized content-type' do
|
75
|
+
expect{ subject.encode('xyzzy', representation) }.
|
76
|
+
to raise_error(ArgumentError)
|
77
|
+
end
|
78
|
+
|
79
|
+
it 'ignores junk after the content type for encoding' do
|
80
|
+
subject.encode('text/plain foo=bar', representation).should eq(representation.to_s)
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'ignores whitespace in the content type for encoding' do
|
84
|
+
subject.encode(' text/plain foo=bar', representation).should eq(representation.to_s)
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'passes options to the JSON encoder' do
|
88
|
+
MultiJson.should_receive(:encode).
|
89
|
+
with(anything, {foo: 42})
|
90
|
+
subject.encode('application/json', representation, foo: 42)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,136 @@
|
|
1
|
+
require 'spec_helper_lite'
|
2
|
+
require 'leadlight/hyperlinkable'
|
3
|
+
|
4
|
+
module Leadlight
|
5
|
+
describe Hyperlinkable do
|
6
|
+
subject { representation.extend(Hyperlinkable) }
|
7
|
+
let(:representation) { Object.new }
|
8
|
+
let(:response) { stub(:response, env: env) }
|
9
|
+
let(:response_url) { Addressable::URI.parse('/foo') }
|
10
|
+
let(:request) { stub(:request) }
|
11
|
+
let(:env) {
|
12
|
+
{
|
13
|
+
url: '/foo',
|
14
|
+
leadlight_service: service,
|
15
|
+
:response_headers => headers
|
16
|
+
}
|
17
|
+
}
|
18
|
+
let(:service) { stub(url: 'http://example.com') }
|
19
|
+
let(:headers) { {'Link' => links} }
|
20
|
+
let(:links) {
|
21
|
+
'<https://api.github.com/users/avdi/followers?page=2>; rel="next",'\
|
22
|
+
' <https://api.github.com/users/avdi/followers?page=6>; rel="last"'
|
23
|
+
}
|
24
|
+
|
25
|
+
before do
|
26
|
+
representation.stub(__response__: response, __service__: service)
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '.extend' do
|
30
|
+
|
31
|
+
it 'adds itself to the representation' do
|
32
|
+
subject.should be_a(Hyperlinkable)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'adds a self link' do
|
36
|
+
subject.link('self').should be_a(Link)
|
37
|
+
subject.link('self').href.to_s.should eq('/foo')
|
38
|
+
subject.link('self').rel.should eq('self')
|
39
|
+
subject.link('self').rev.should eq('self')
|
40
|
+
subject.link('self').title.should eq('self')
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'adds links from Link header' do
|
44
|
+
subject.link('next').href.to_s.
|
45
|
+
should eq('https://api.github.com/users/avdi/followers?page=2')
|
46
|
+
subject.link('last').href.to_s.
|
47
|
+
should eq('https://api.github.com/users/avdi/followers?page=6')
|
48
|
+
end
|
49
|
+
|
50
|
+
end
|
51
|
+
|
52
|
+
describe '#add_link' do
|
53
|
+
it 'swells the links collection' do
|
54
|
+
expect { subject.add_link('/foo') }.to change{ subject.links.size }.by(1)
|
55
|
+
end
|
56
|
+
|
57
|
+
it 'expands links' do
|
58
|
+
subject.add_link('/parent', 'parent')
|
59
|
+
subject.link('parent').href.to_s.should eq('/parent')
|
60
|
+
end
|
61
|
+
|
62
|
+
it 'adds a Link' do
|
63
|
+
subject.add_link('/parent', 'parent')
|
64
|
+
subject.link('parent').should be_a(Link)
|
65
|
+
end
|
66
|
+
|
67
|
+
it 'adds a link helper' do
|
68
|
+
subject.add_link('/parent', 'parent')
|
69
|
+
service.should_receive(:get_representation!).
|
70
|
+
with(Addressable::URI.parse('/parent')).
|
71
|
+
and_return(request)
|
72
|
+
subject.parent
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '#add_link_template' do
|
77
|
+
it 'adds a link' do
|
78
|
+
expect { subject.add_link_template('/foo/{id}') }.to change{ subject.links.size }.by(1)
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'expands links' do
|
82
|
+
subject.add_link_template('/child/{index}', 'child')
|
83
|
+
href = subject.link('child').href
|
84
|
+
href.to_s.should eq('/child/{index}')
|
85
|
+
end
|
86
|
+
|
87
|
+
it 'adds a LinkTemplate' do
|
88
|
+
subject.add_link_template('/child/{index}', 'child')
|
89
|
+
subject.link('child').should be_a(LinkTemplate)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'adds a link helper' do
|
93
|
+
subject.add_link_template('/child/{index}', 'child')
|
94
|
+
service.should_receive(:get_representation!).
|
95
|
+
with('/child/23')
|
96
|
+
subject.child(23)
|
97
|
+
end
|
98
|
+
|
99
|
+
end
|
100
|
+
|
101
|
+
describe '#add_link_set' do
|
102
|
+
before do
|
103
|
+
subject.add_link_set('bourbon') do
|
104
|
+
[
|
105
|
+
{href: 'http://example.com/blantons', title: 'Blantons'},
|
106
|
+
{href: 'http://example.com/bookers', title: 'Bookers'},
|
107
|
+
{href: 'http://example.com/knobcreek', title: 'Knob Creek', aliases: ['KC']},
|
108
|
+
]
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
it 'adds links for each member of the set' do
|
113
|
+
links = subject.links('bourbon').map(&:href).map(&:to_s)
|
114
|
+
links.should include('http://example.com/blantons')
|
115
|
+
links.should include('http://example.com/bookers')
|
116
|
+
links.should include('http://example.com/knobcreek')
|
117
|
+
end
|
118
|
+
|
119
|
+
it 'establishes a link helper that finds by title' do
|
120
|
+
result_stubs = [stub, stub, stub]
|
121
|
+
service.should_receive(:get_representation!).and_yield(result_stubs[0])
|
122
|
+
service.should_receive(:get_representation!).and_yield(result_stubs[1])
|
123
|
+
service.should_receive(:get_representation!).and_yield(result_stubs[2])
|
124
|
+
subject.bourbon('Blantons').should equal(result_stubs[0])
|
125
|
+
subject.bourbon('Bookers').should equal(result_stubs[1])
|
126
|
+
subject.bourbon('Knob Creek').should equal(result_stubs[2])
|
127
|
+
end
|
128
|
+
|
129
|
+
it 'sets up the link helper to handle aliases' do
|
130
|
+
result = stub
|
131
|
+
service.should_receive(:get_representation!).and_yield(result)
|
132
|
+
subject.bourbon('KC').should equal(result)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper_lite'
|
2
|
+
require 'leadlight/link'
|
3
|
+
|
4
|
+
module Leadlight
|
5
|
+
describe Link do
|
6
|
+
subject { Link.new(service, href, rel, title, options) }
|
7
|
+
let(:service) { stub(:service, get: nil) }
|
8
|
+
let(:href) { '/TEST_PATH' }
|
9
|
+
let(:rel) { 'TEST_REL' }
|
10
|
+
let(:title) { 'TEST_TITLE' }
|
11
|
+
let(:options) { {} }
|
12
|
+
|
13
|
+
describe '#follow' do
|
14
|
+
it 'calls service.get with the href' do
|
15
|
+
service.should_receive(:get_representation!).with(Addressable::URI.parse(href))
|
16
|
+
subject.follow
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'returns the result of the get' do
|
20
|
+
request = stub
|
21
|
+
result = stub
|
22
|
+
service.should_receive(:get_representation!).
|
23
|
+
and_yield(result).
|
24
|
+
and_return(request)
|
25
|
+
subject.follow.should equal(result)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.delegates_to_service(method_name)
|
30
|
+
it "delegates ##{method_name} to the service" do
|
31
|
+
yielded = :nothing
|
32
|
+
args = [:foo, :bar]
|
33
|
+
request = stub(:request)
|
34
|
+
representation = stub(:representation)
|
35
|
+
service.should_receive(method_name).
|
36
|
+
with(subject.href, *args).
|
37
|
+
and_yield(representation).
|
38
|
+
and_return(request)
|
39
|
+
subject.public_send(method_name, *args) do |rep|
|
40
|
+
yielded = rep
|
41
|
+
end.should equal(request)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
delegates_to_service :get
|
46
|
+
delegates_to_service :post
|
47
|
+
delegates_to_service :head
|
48
|
+
delegates_to_service :options
|
49
|
+
delegates_to_service :put
|
50
|
+
delegates_to_service :delete
|
51
|
+
delegates_to_service :patch
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'spec_helper_lite'
|
2
|
+
require 'leadlight/link_template'
|
3
|
+
|
4
|
+
module Leadlight
|
5
|
+
describe LinkTemplate do
|
6
|
+
|
7
|
+
# TODO: This setup is loony. Refactor.
|
8
|
+
subject { LinkTemplate.new(service, href, rel, title, options) }
|
9
|
+
let(:service) { stub(:service) }
|
10
|
+
let(:request) { stub(:request) }
|
11
|
+
let(:result) { stub(:result) }
|
12
|
+
let(:href) { '/TEST_PATH/{n}/{m}/' }
|
13
|
+
let(:rel) { 'TEST_REL' }
|
14
|
+
let(:title) { 'TEST_TITLE' }
|
15
|
+
let(:options) { {} }
|
16
|
+
let(:mapping) { {'n' => 'N_VALUE', 'm' => 'M_VALUE'} }
|
17
|
+
let(:values) { mapping.values }
|
18
|
+
|
19
|
+
before do
|
20
|
+
request.stub(raise_on_error: request)
|
21
|
+
service.stub(:get_representation!).and_yield(result).and_return(request)
|
22
|
+
end
|
23
|
+
|
24
|
+
describe '#follow' do
|
25
|
+
it 'calls service.get with the expanded href' do
|
26
|
+
service.should_receive(:get_representation!).with('/TEST_PATH/23/42/')
|
27
|
+
subject.follow(23,42)
|
28
|
+
end
|
29
|
+
|
30
|
+
it 'can accept a hash for template parameters' do
|
31
|
+
service.should_receive(:get_representation!).with('/TEST_PATH/AA/BB/')
|
32
|
+
subject.follow(:n => 'AA', 'm' => 'BB')
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'leaves unrecognized params in the params hash alone' do
|
36
|
+
service.should_receive(:get_representation!).with('/TEST_PATH/AA/BB/', {'other' => 'XX'})
|
37
|
+
subject.follow(:n => 'AA', 'm' => 'BB', :other => 'XX')
|
38
|
+
end
|
39
|
+
|
40
|
+
it 'returns the result of the get' do
|
41
|
+
subject.follow(23,42).should equal(result)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper_lite'
|
2
|
+
require 'leadlight/representation'
|
3
|
+
|
4
|
+
module Leadlight
|
5
|
+
describe Representation do
|
6
|
+
module TintA; end
|
7
|
+
module TintB; end
|
8
|
+
|
9
|
+
subject { object }
|
10
|
+
let(:object) { Object.new.extend(Representation) }
|
11
|
+
let(:service) { stub(:service, tints: [TintA, TintB]) }
|
12
|
+
let(:location) { stub(:location) }
|
13
|
+
let(:response) { stub(:response) }
|
14
|
+
|
15
|
+
it 'has a __service__ accessor' do
|
16
|
+
subject.__service__ = service
|
17
|
+
subject.__service__.should equal(service)
|
18
|
+
end
|
19
|
+
|
20
|
+
it 'has a __location__ accessor' do
|
21
|
+
subject.__location__ = location
|
22
|
+
subject.__location__.should equal(location)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'has a __response__ accessor' do
|
26
|
+
subject.__response__ = response
|
27
|
+
subject.__response__.should equal(response)
|
28
|
+
end
|
29
|
+
|
30
|
+
describe '#initialize_representation' do
|
31
|
+
it 'sets __service__, __location__, and __response__' do
|
32
|
+
subject.initialize_representation(service, location, response)
|
33
|
+
subject.__service__.should equal(service)
|
34
|
+
subject.__location__.should equal(location)
|
35
|
+
subject.__response__.should equal(response)
|
36
|
+
end
|
37
|
+
|
38
|
+
it 'returns self' do
|
39
|
+
result = subject.initialize_representation(service, location, response)
|
40
|
+
result.should equal(subject)
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
describe '#apply_all_tints' do
|
45
|
+
before do
|
46
|
+
subject.initialize_representation(service, location, response)
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'extends object with tints from service' do
|
50
|
+
subject.apply_all_tints
|
51
|
+
subject.should be_a(TintA)
|
52
|
+
subject.should be_a(TintB)
|
53
|
+
end
|
54
|
+
|
55
|
+
it 'applies tints' do
|
56
|
+
object.should_receive(:__apply_tint__)
|
57
|
+
subject.apply_all_tints
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|