croudia 0.0.2 → 1.0.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.
Files changed (66) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +2 -0
  3. data/README.md +31 -14
  4. data/Rakefile +11 -11
  5. data/croudia.gemspec +10 -8
  6. data/lib/croudia/api/favorites.rb +43 -0
  7. data/lib/croudia/api/oauth.rb +32 -0
  8. data/lib/croudia/api/statuses.rb +55 -0
  9. data/lib/croudia/api/timelines.rb +57 -0
  10. data/lib/croudia/base.rb +44 -0
  11. data/lib/croudia/client.rb +64 -0
  12. data/lib/croudia/configurable.rb +39 -0
  13. data/lib/croudia/creatable.rb +9 -0
  14. data/lib/croudia/default.rb +63 -0
  15. data/lib/croudia/ext/openssl.rb +19 -0
  16. data/lib/croudia/identity.rb +22 -39
  17. data/lib/croudia/status.rb +26 -0
  18. data/lib/croudia/user.rb +12 -43
  19. data/lib/croudia/version.rb +1 -1
  20. data/lib/croudia.rb +21 -12
  21. data/spec/croudia/api/favorites_spec.rb +35 -0
  22. data/spec/croudia/api/oauth_spec.rb +53 -0
  23. data/spec/croudia/api/statuses_spec.rb +73 -0
  24. data/spec/croudia/api/timelines_spec.rb +93 -0
  25. data/spec/croudia/base_spec.rb +27 -0
  26. data/spec/croudia/client_spec.rb +115 -0
  27. data/spec/croudia/identity_spec.rb +29 -65
  28. data/spec/croudia/status_spec.rb +30 -0
  29. data/spec/croudia/user_spec.rb +30 -43
  30. data/spec/croudia_spec.rb +48 -49
  31. data/spec/fixtures/access_token.json +6 -0
  32. data/spec/fixtures/status.json +1 -0
  33. data/spec/fixtures/timeline.json +1 -0
  34. data/spec/helper.rb +49 -32
  35. metadata +80 -84
  36. data/lib/croudia/api.rb +0 -5
  37. data/lib/croudia/error.rb +0 -5
  38. data/lib/croudia/scraper/friendships.rb +0 -72
  39. data/lib/croudia/scraper/login.rb +0 -42
  40. data/lib/croudia/scraper/parser/users.rb +0 -59
  41. data/lib/croudia/scraper/parser/voices.rb +0 -32
  42. data/lib/croudia/scraper/parser.rb +0 -13
  43. data/lib/croudia/scraper/users.rb +0 -20
  44. data/lib/croudia/scraper/voices.rb +0 -59
  45. data/lib/croudia/scraper.rb +0 -63
  46. data/lib/croudia/voice.rb +0 -21
  47. data/spec/croudia/api_spec.rb +0 -7
  48. data/spec/croudia/error_spec.rb +0 -13
  49. data/spec/croudia/scraper/friendships_spec.rb +0 -115
  50. data/spec/croudia/scraper/login_spec.rb +0 -137
  51. data/spec/croudia/scraper/parser/users_spec.rb +0 -292
  52. data/spec/croudia/scraper/parser/voices_spec.rb +0 -119
  53. data/spec/croudia/scraper/users_spec.rb +0 -78
  54. data/spec/croudia/scraper/voices_spec.rb +0 -178
  55. data/spec/croudia/scraper_spec.rb +0 -120
  56. data/spec/croudia/version_spec.rb +0 -7
  57. data/spec/croudia/voice_spec.rb +0 -16
  58. data/spec/fixtures/follow_request.html +0 -69
  59. data/spec/fixtures/follower_wktk.html +0 -89
  60. data/spec/fixtures/following_wktk.html +0 -89
  61. data/spec/fixtures/user_wktk1.html +0 -72
  62. data/spec/fixtures/user_wktk2.html +0 -75
  63. data/spec/fixtures/user_wktk3.html +0 -83
  64. data/spec/fixtures/voices_reply_list.html +0 -55
  65. data/spec/fixtures/voices_timeline.html +0 -60
  66. data/spec/fixtures/voices_written.html +0 -20
data/lib/croudia/user.rb CHANGED
@@ -1,43 +1,12 @@
1
- require 'croudia/identity'
2
-
3
- module Croudia
4
- class User < Croudia::Identity
5
- attr_reader :username, :nickname, :avatar, :self_introduction,
6
- :voices_count, :following_count, :followers_count, :album_count,
7
- :position, :user_url, :spreadia, :favodia,
8
- :following, :followed_by
9
- alias handle username
10
- alias screen_name username
11
- alias user_name username
12
- alias name nickname
13
- alias profile_image avatar
14
- alias description self_introduction
15
- alias voice_count voices_count
16
- alias statuses_count voices_count
17
- alias status_count voices_count
18
- alias updates_count voices_count
19
- alias update_count voices_count
20
- alias followings_count following_count
21
- alias friends_count following_count
22
- alias friend_count following_count
23
- alias follower_count followers_count
24
- alias location position
25
- alias url user_url
26
- alias favorited_count favodia
27
- alias favourited_count favodia
28
- alias spreaded_count spreadia
29
- alias following? following
30
- alias followed_by? followed_by
31
-
32
- def initialize(*args)
33
- super
34
- @id = username
35
- @spreadia = spreadia.to_i if spreadia
36
- @favodia = favodia.to_i if favodia
37
- end
38
-
39
- def to_s
40
- username
41
- end
42
- end
43
- end
1
+ require 'croudia/creatable'
2
+ require 'croudia/identity'
3
+
4
+ module Croudia
5
+ class User < Croudia::Identity
6
+ include Croudia::Creatable
7
+
8
+ attr_reader :description, :favorites_count, :follow_request_sent,
9
+ :followers_count, :fruends_count, :location, :name, :profile_image_url_https,
10
+ :screen_name, :statuses_count, :url
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module Croudia
2
- VERSION = '0.0.2'
2
+ VERSION = '1.0.0'
3
3
  end
data/lib/croudia.rb CHANGED
@@ -1,23 +1,32 @@
1
- require 'croudia/scraper'
1
+ require 'croudia/client'
2
+ require 'croudia/configurable'
3
+ require 'croudia/version'
2
4
 
3
5
  module Croudia
4
6
  class << self
5
- # Delegate to Croudia::Scraper.new
7
+ include Croudia::Configurable
8
+
9
+ # Delegate to a Croudia::Client
6
10
  #
7
- # @return [Croudia::Scraper]
8
- def scraper(*args, &block)
9
- Croudia::Scraper.new(*args, &block)
11
+ # @return [Croudia::Client]
12
+ def client
13
+ if !@client || @client.hash != options.hash
14
+ @client = Croudia::Client.new
15
+ end
16
+ @client
10
17
  end
11
- alias new scraper
12
- alias client scraper
13
18
 
14
- def method_missing(name, *args, &block)
15
- return super unless scraper.respond_to?(name)
16
- scraper.send(name, *args, &block)
19
+ def respond_to?(*args)
20
+ super || client.respond_to?(*args)
17
21
  end
18
22
 
19
- def respond_to?(name)
20
- scraper.respond_to?(name) || super
23
+ private
24
+
25
+ def method_missing(name, *args, &block)
26
+ return super unless client.respond_to?(name)
27
+ client.send(name, *args, &block)
21
28
  end
22
29
  end
23
30
  end
31
+
32
+ Croudia.setup
@@ -0,0 +1,35 @@
1
+ require 'helper'
2
+
3
+ describe Croudia::API::Favorites do
4
+ before do
5
+ @client = Croudia::Client.new
6
+ end
7
+
8
+ describe '#favorite' do
9
+ before do
10
+ stub_post('/favorites/create/1234.json').to_return(
11
+ body: fixture(:status),
12
+ headers: { content_type: 'application/json; charset=utf-8' }
13
+ )
14
+ end
15
+
16
+ it 'requests the correct resource' do
17
+ @client.favorite(1234)
18
+ expect(a_post('/favorites/create/1234.json')).to have_been_made
19
+ end
20
+ end
21
+
22
+ describe '#unfavorite' do
23
+ before do
24
+ stub_delete('/favorites/destroy/1234.json').to_return(
25
+ body: fixture(:status),
26
+ headers: { content_type: 'application/json; charset=utf-8' }
27
+ )
28
+ end
29
+
30
+ it 'requests the correct resource' do
31
+ @client.unfavorite(1234)
32
+ expect(a_delete('/favorites/destroy/1234.json')).to have_been_made
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,53 @@
1
+ require 'cgi'
2
+ require 'helper'
3
+ require 'uri'
4
+
5
+ describe Croudia::API::OAuth do
6
+ before do
7
+ options = { client_id: 'cid-value', client_secret: 'cs-value' }
8
+ @client = Croudia::Client.new(options)
9
+ end
10
+
11
+ describe '#authorize_url' do
12
+ it 'returns the correct url' do
13
+ uri = URI.parse(@client.authorize_url)
14
+ expect(uri.path).to eq '/oauth/authorize'
15
+ expect(CGI.parse(uri.query)).to eq({
16
+ 'response_type' => ['code'],
17
+ 'client_id' => ['cid-value'],
18
+ })
19
+ end
20
+
21
+ it 'does not include the client_secret in URL' do
22
+ url = @client.authorize_url
23
+ expect(url).not_to include('cs-value')
24
+ end
25
+
26
+ it 'includes argument hash to the URL' do
27
+ url = @client.authorize_url(hoge: 'fuga', foo: :bar)
28
+ query = CGI.parse(URI.parse(url).query)
29
+ expect(query['hoge']).to include('fuga')
30
+ expect(query['foo']).to include('bar')
31
+ end
32
+ end
33
+
34
+ describe '#get_access_token' do
35
+ before do
36
+ stub_post('/oauth/token').to_return(
37
+ body: fixture(:access_token),
38
+ headers: { content_type: 'application/json; charset=utf-8' }
39
+ )
40
+ end
41
+
42
+ it 'requests the correct resource' do
43
+ @client.get_access_token(code: 'code-value')
44
+ expect(a_post('/oauth/token').with(
45
+ body: {
46
+ code: 'code-value',
47
+ grant_type: 'authorization_code',
48
+ client_id: 'cid-value',
49
+ client_secret: 'cs-value',
50
+ })).to have_been_made
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,73 @@
1
+ require 'helper'
2
+
3
+ describe Croudia::API::Statuses do
4
+ before do
5
+ @client = Croudia::Client.new
6
+ end
7
+
8
+ describe '#update' do
9
+ before do
10
+ stub_post('/statuses/update.json').with(
11
+ body: {
12
+ status: 'Hello',
13
+ in_reply_to_status_id: '1234',
14
+ }
15
+ ).to_return(
16
+ body: fixture(:status),
17
+ headers: { content_type: 'application/json; charset=utf-8' }
18
+ )
19
+ end
20
+
21
+ context 'when text is passed as a string' do
22
+ it 'requests the correct resource' do
23
+ @client.update('Hello', in_reply_to_status_id: 1234)
24
+ expect(a_post('/statuses/update.json').with(
25
+ body: {
26
+ status: 'Hello',
27
+ in_reply_to_status_id: '1234',
28
+ }
29
+ )).to have_been_made
30
+ end
31
+ end
32
+
33
+ context 'when text is passed as a value of hash' do
34
+ it 'requests the correct resource' do
35
+ @client.update(status: 'Hello', in_reply_to_status_id: 1234)
36
+ expect(a_post('/statuses/update.json').with(
37
+ body: {
38
+ status: 'Hello',
39
+ in_reply_to_status_id: '1234',
40
+ }
41
+ )).to have_been_made
42
+ end
43
+ end
44
+ end
45
+
46
+ describe '#destroy_status' do
47
+ before do
48
+ stub_post('/statuses/destroy/1234.json').to_return(
49
+ body: fixture(:status),
50
+ headers: { content_type: 'application/json; charset=utf-8' }
51
+ )
52
+ end
53
+
54
+ it 'requests the correct resource' do
55
+ @client.destroy_status(1234)
56
+ expect(a_post('/statuses/destroy/1234.json')).to have_been_made
57
+ end
58
+ end
59
+
60
+ describe '#status' do
61
+ before do
62
+ stub_get('/statuses/show/1234.json').to_return(
63
+ body: fixture(:status),
64
+ header: { content_type: 'application/json; charset=utf-8' }
65
+ )
66
+ end
67
+
68
+ it 'requests the correct resource' do
69
+ @client.status(1234)
70
+ expect(a_get('/statuses/show/1234.json')).to have_been_made
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,93 @@
1
+ require 'helper'
2
+
3
+ describe Croudia::API::Timelines do
4
+ before do
5
+ @client = Croudia::Client.new
6
+ end
7
+
8
+ describe '#public_timeline' do
9
+ before do
10
+ stub_get('/statuses/public_timeline.json').to_return(
11
+ body: fixture(:timeline),
12
+ headers: { content_type: 'application/json; charset=utf-8' }
13
+ )
14
+ end
15
+
16
+ it 'requests the correct resource' do
17
+ @client.public_timeline
18
+ expect(a_get('/statuses/public_timeline.json')).to have_been_made
19
+ end
20
+ end
21
+
22
+ describe '#home_timeline' do
23
+ before do
24
+ stub_get('/statuses/home_timeline.json').to_return(
25
+ body: fixture(:timeline),
26
+ headers: { content_type: 'application/json; charset=utf-8' }
27
+ )
28
+ end
29
+
30
+ it 'requests the correct resource' do
31
+ @client.home_timeline
32
+ expect(a_get('/statuses/home_timeline.json')).to have_been_made
33
+ end
34
+ end
35
+
36
+ describe '#user_timeline' do
37
+ context 'without a screen_name' do
38
+ it 'is not supported' do
39
+ expect { @client.user_timeline }.to raise_error ArgumentError
40
+ end
41
+ end
42
+
43
+ context 'with a screen_name (string)' do
44
+ before do
45
+ stub_get('/statuses/user_timeline.json').with(
46
+ query: { screen_name: 'wktk' }
47
+ ).to_return(
48
+ body: fixture(:timeline),
49
+ headers: { content_type: 'application/json; charset=utf-8' }
50
+ )
51
+ end
52
+
53
+ it 'requests the correct resource with the screen_name passed' do
54
+ @client.user_timeline('wktk')
55
+ expect(a_get('/statuses/user_timeline.json').with(
56
+ query: { screen_name: 'wktk' }
57
+ )).to have_been_made
58
+ end
59
+ end
60
+
61
+ context 'with a user_id (integer)' do
62
+ before do
63
+ stub_get('/statuses/user_timeline.json').with(
64
+ query: { user_id: 1234 }
65
+ ).to_return(
66
+ body: fixture(:timeline),
67
+ headers: { content_type: 'application/json; charset=utf-8' }
68
+ )
69
+ end
70
+
71
+ it 'requests the correct resource with the user_id passed' do
72
+ @client.user_timeline(1234)
73
+ expect(a_get('/statuses/user_timeline.json').with(
74
+ query: { user_id: 1234 }
75
+ )).to have_been_made
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '#mentions' do
81
+ before do
82
+ stub_get('/statuses/mentions.json').to_return(
83
+ body: fixture(:timeline),
84
+ headers: { content_type: 'application/json; charset=utf-8' }
85
+ )
86
+ end
87
+
88
+ it 'requests the correct resource' do
89
+ @client.mentions
90
+ expect(a_get('/statuses/mentions.json')).to have_been_made
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,27 @@
1
+ require 'helper'
2
+
3
+ describe Croudia::Base do
4
+ before do
5
+ @base = Croudia::Base.new(id: 1)
6
+ end
7
+
8
+ describe '#[]' do
9
+ it 'calls methods using [] with symbol' do
10
+ expect(@base[:object_id]).to be_an Integer
11
+ end
12
+
13
+ it 'calls methods using [] with string' do
14
+ expect(@base['object_id']).to be_an Integer
15
+ end
16
+
17
+ it 'returns nil for missing method' do
18
+ expect(@base[:a_missing_method_hoge]).to be_nil
19
+ end
20
+ end
21
+
22
+ describe '#attrs' do
23
+ it 'returns a hash of attributes' do
24
+ expect(@base.attrs).to eq({id: 1})
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,115 @@
1
+ require 'helper'
2
+
3
+ describe Croudia::Client do
4
+ context 'with module counfiguration' do
5
+ before do
6
+ Croudia.configure do |config|
7
+ Croudia::Configurable.keys.each do |key|
8
+ config.send("#{key}=", key)
9
+ end
10
+ end
11
+ end
12
+
13
+ after do
14
+ Croudia.reset!
15
+ end
16
+
17
+ it 'inherits the module configuration' do
18
+ client = Croudia::Client.new
19
+ Croudia::Configurable.keys.each do |key|
20
+ expect(client.instance_variable_get(:"@#{key}")).to eq key
21
+ end
22
+ end
23
+ end
24
+
25
+ context 'with class configuration' do
26
+ before do
27
+ @configuration = {
28
+ connection_options: { timeout: 10 },
29
+ client_id: 'CK',
30
+ client_secret: 'CS',
31
+ endpoint: 'http://twitter.com',
32
+ middleware: Proc.new { },
33
+ access_token: 'AT',
34
+ }
35
+ end
36
+
37
+ context 'during initialization' do
38
+ it 'overrides the modue configuration' do
39
+ client = Croudia::Client.new(@configuration)
40
+ Croudia::Configurable.keys.each do |key|
41
+ expect(client.instance_variable_get(:"@#{key}")).to eq @configuration[key]
42
+ end
43
+ end
44
+ end
45
+
46
+ context 'after initialization' do
47
+ it 'overrides the module configuration after initialization' do
48
+ client = Croudia::Client.new
49
+ client.configure do |config|
50
+ @configuration.each do |key, value|
51
+ config.send("#{key}=", value)
52
+ end
53
+ end
54
+ Croudia::Configurable.keys.each do |key|
55
+ expect(client.instance_variable_get(:"@#{key}")).to eq @configuration[key]
56
+ end
57
+ end
58
+ end
59
+ end
60
+
61
+ describe '#delete' do
62
+ before do
63
+ stub_delete('/delete').with(query: { deleted: 'object' })
64
+ end
65
+
66
+ it 'allows custom delete requests' do
67
+ Croudia::Client.new.delete('/delete', { deleted: 'object' })
68
+ expect(a_delete('/delete').with(
69
+ query: { deleted: 'object' }
70
+ )).to have_been_made
71
+ end
72
+ end
73
+
74
+ describe '#get' do
75
+ before do
76
+ stub_get('/get').with(query: { hoge: 'fuga' })
77
+ end
78
+
79
+ it 'allows custom get requests' do
80
+ Croudia::Client.new.get('/get', { hoge: 'fuga' })
81
+ expect(a_get('/get').with(
82
+ query: { hoge: 'fuga'}
83
+ )).to have_been_made
84
+ end
85
+ end
86
+
87
+ describe '#post' do
88
+ before do
89
+ stub_post('/post').with(body: { posted: 'object' })
90
+ end
91
+
92
+ it 'allows custom post requests' do
93
+ Croudia::Client.new.post('/post', { posted: 'object' })
94
+ expect(a_post('/post').with(
95
+ body: { posted: 'object' }
96
+ )).to have_been_made
97
+ end
98
+ end
99
+
100
+ describe '#put' do
101
+ before do
102
+ stub_put('/put').with(body: { updated: 'object'})
103
+ end
104
+
105
+ it 'allows custom delete requests' do
106
+ Croudia::Client.new.put('/put', { updated: 'object' })
107
+ expect(a_put('/put').with(
108
+ body: { updated: 'object' }
109
+ )).to have_been_made
110
+ end
111
+ end
112
+
113
+ describe '#request' do
114
+ end
115
+ end
@@ -1,65 +1,29 @@
1
- require 'helper'
2
-
3
- describe Croudia::Identity do
4
- describe '#initialize' do
5
- it 'sets arguments as instance variables' do
6
- Croudia::Identity.new(:hello => 'hi').instance_variable_get('@hello').should eq 'hi'
7
- end
8
-
9
- it 'converts a Unix time attribution named time to a Time object' do
10
- Croudia::Identity.new(:time => '1234567890').time.should be_a Time
11
- end
12
-
13
- it 'converts a date string named time to a Time object' do
14
- Croudia::Identity.new(:time => '2013-01-01 11:55').time.should be_a Time
15
- end
16
-
17
- it 'converts *_count into an Integer' do
18
- Croudia::Identity.new(:foo_count => '2012').foo_count.should eq 2012
19
- end
20
- end
21
-
22
- describe '#==' do
23
- it 'returns true when IDs match' do
24
- obj1 = Croudia::Identity.new(:id => '1', :username => 'wktk')
25
- obj2 = Croudia::Identity.new(:id => '1', :username => 'croudia')
26
- (obj1 == obj2).should be_true
27
- end
28
-
29
- it 'returns false when IDs do not match' do
30
- obj1 = Croudia::Identity.new(:id => '1', :username => 'wktk')
31
- obj2 = Croudia::Identity.new(:id => '2', :username => 'wktk')
32
- (obj1 == obj2).should be_false
33
- end
34
-
35
- it 'returns false when classes do not match' do
36
- obj1 = Croudia::Identity.new(:id => '1')
37
- obj2 = { :id => '1' }
38
- (obj1 == obj2).should be_false
39
- end
40
- end
41
-
42
- describe '#[]' do
43
- it 'returns the same name instance variable' do
44
- Croudia::Identity.new(:foo => 'bar')[:foo].should eq 'bar'
45
- end
46
- end
47
-
48
- describe '#id' do
49
- it 'returns @id, not object_id' do
50
- identity = Croudia::Identity.new(:id => '1')
51
- identity.id.should_not eq identity.object_id
52
- identity.id.should eq identity.instance_variable_get('@id')
53
- end
54
- end
55
-
56
- describe '#method_missing' do
57
- it 'returns the same name instance variable if set' do
58
- Croudia::Identity.new(:foo => 'bar').foo.should eq 'bar'
59
- end
60
-
61
- it 'returns nil if not set' do
62
- Croudia::Identity.new(:foo => 'bar').bar.should be_nil
63
- end
64
- end
65
- end
1
+ require 'helper'
2
+
3
+ describe Croudia::Identity do
4
+ describe '#initialize' do
5
+ it 'raises ArguentError if :id attr is missing' do
6
+ expect { Croudia::Identity.new }.to raise_error ArgumentError
7
+ end
8
+ end
9
+
10
+ describe '#==' do
11
+ it 'returns true when objects IDs are the same' do
12
+ one = Croudia::Identity.new(id: 1, text: 'hoge')
13
+ two = Croudia::Identity.new(id: 1, text: 'fuga')
14
+ expect(one == two).to be_true
15
+ end
16
+
17
+ it 'returns false when objects IDs are different' do
18
+ one = Croudia::Identity.new(id: 1)
19
+ two = Croudia::Identity.new(id: 2)
20
+ expect(one == two).to be_false
21
+ end
22
+
23
+ it 'returns false when classes are different' do
24
+ one = Croudia::Identity.new(id: 1)
25
+ two = Croudia::Base.new(id: 1)
26
+ expect(one == two).to be_false
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,30 @@
1
+ require 'helper'
2
+
3
+ describe Croudia::Status do
4
+ describe '#==' do
5
+ it 'returns true when objects IDs are the same' do
6
+ status = Croudia::Status.new(id: 1, text: 'hoge')
7
+ other = Croudia::Status.new(id: 1, text: 'fuga')
8
+ expect(status == other).to be_true
9
+ end
10
+
11
+ it 'returns false when objects IDs are different' do
12
+ status = Croudia::Status.new(id: 1)
13
+ other = Croudia::Status.new(id: 2)
14
+ expect(status == other).to be_false
15
+ end
16
+
17
+ it 'returns false when classes are different' do
18
+ status = Croudia::Status.new(id: 1)
19
+ other = Croudia::Identity.new(id: 1)
20
+ expect(status == other).to be_false
21
+ end
22
+ end
23
+
24
+ describe '#created_at' do
25
+ it 'returns a Time' do
26
+ status = Croudia::Status.new(id: 3, created_at: 'Mon, 08 Jul 2013 01:23:45 +0900')
27
+ expect(status.created_at).to be_a Time
28
+ end
29
+ end
30
+ end