rack-ketai 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (44) hide show
  1. data/.gitignore +1 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README.rdoc +122 -0
  4. data/VERSION +1 -0
  5. data/lib/rack/ketai/carrier/abstract.rb +263 -0
  6. data/lib/rack/ketai/carrier/au.rb +153 -0
  7. data/lib/rack/ketai/carrier/cidrs/au.rb +32 -0
  8. data/lib/rack/ketai/carrier/cidrs/docomo.rb +14 -0
  9. data/lib/rack/ketai/carrier/cidrs/softbank.rb +10 -0
  10. data/lib/rack/ketai/carrier/docomo.rb +157 -0
  11. data/lib/rack/ketai/carrier/emoji/ausjisstrtoemojiid.rb +1391 -0
  12. data/lib/rack/ketai/carrier/emoji/docomosjisstrtoemojiid.rb +759 -0
  13. data/lib/rack/ketai/carrier/emoji/emojidata.rb +836 -0
  14. data/lib/rack/ketai/carrier/emoji/emojiidtotypecast.rb +432 -0
  15. data/lib/rack/ketai/carrier/emoji/softbankutf8strtoemojiid.rb +1119 -0
  16. data/lib/rack/ketai/carrier/emoji/softbankwebcodetoutf8str.rb +499 -0
  17. data/lib/rack/ketai/carrier/general.rb +75 -0
  18. data/lib/rack/ketai/carrier/iphone.rb +16 -0
  19. data/lib/rack/ketai/carrier/softbank.rb +144 -0
  20. data/lib/rack/ketai/carrier/specs/au.rb +1 -0
  21. data/lib/rack/ketai/carrier/specs/docomo.rb +1 -0
  22. data/lib/rack/ketai/carrier/specs/softbank.rb +1 -0
  23. data/lib/rack/ketai/carrier.rb +18 -0
  24. data/lib/rack/ketai/display.rb +16 -0
  25. data/lib/rack/ketai/middleware.rb +36 -0
  26. data/lib/rack/ketai.rb +12 -0
  27. data/spec/spec_helper.rb +11 -0
  28. data/spec/unit/au_filter_spec.rb +96 -0
  29. data/spec/unit/au_spec.rb +240 -0
  30. data/spec/unit/carrier_spec.rb +30 -0
  31. data/spec/unit/display_spec.rb +25 -0
  32. data/spec/unit/docomo_filter_spec.rb +106 -0
  33. data/spec/unit/docomo_spec.rb +344 -0
  34. data/spec/unit/emoticon_filter_spec.rb +91 -0
  35. data/spec/unit/filter_spec.rb +38 -0
  36. data/spec/unit/iphone_spec.rb +16 -0
  37. data/spec/unit/middleware_spec.rb +38 -0
  38. data/spec/unit/softbank_filter_spec.rb +133 -0
  39. data/spec/unit/softbank_spec.rb +200 -0
  40. data/spec/unit/valid_addr_spec.rb +86 -0
  41. data/test/spec_runner.rb +29 -0
  42. data/tools/generate_emoji_dic.rb +434 -0
  43. data/tools/update_speclist.rb +87 -0
  44. metadata +138 -0
@@ -0,0 +1,240 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rack/ketai/carrier/au'
3
+ describe "Rack::Ketai::Carrier::Au" do
4
+
5
+ before(:each) do
6
+
7
+ end
8
+
9
+ describe "WAP2.0ブラウザ搭載端末で" do
10
+
11
+ # http://ke-tai.org/blog/2008/09/08/phoneid/
12
+ # http://www.au.kddi.com/ezfactory/tec/spec/4_4.html
13
+
14
+ describe "EZ番号(サブスクライバID)を取得できたとき" do
15
+
16
+ before(:each) do
17
+ @env = Rack::MockRequest.env_for('http://hoge.com/dummy',
18
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0',
19
+ 'HTTP_X_UP_SUBNO' => '01234567890123_xx.ezweb.ne.jp')
20
+ @mobile = Rack::Ketai::Carrier::Au.new(@env)
21
+ end
22
+
23
+ it "#subscriberid でEZ番号を取得できること" do
24
+ @mobile.subscriberid.should == '01234567890123_xx.ezweb.ne.jp'
25
+ end
26
+
27
+ it "#deviceid は nil なこと" do
28
+ @mobile.deviceid.should be_nil
29
+ end
30
+
31
+ it "#ident でEZ番号を取得できること" do
32
+ @mobile.ident.should == @mobile.subscriberid
33
+ @mobile.ident.should == '01234567890123_xx.ezweb.ne.jp'
34
+ end
35
+
36
+ it "#name で機種名を取得できること" do
37
+ @mobile.name.should == 'SA31'
38
+ end
39
+
40
+ end
41
+
42
+ describe "#cache_size でキャッシュ容量を取得するとき" do
43
+
44
+ it "環境変数を使用すること" do
45
+ env = Rack::MockRequest.env_for('http://hoge.com/dummy',
46
+ 'HTTP_USER_AGENT' => 'KDDI-HI3B UP.Browser/6.2.0.13.2 (GUI) MMP/2.0',
47
+ 'HTTP_X_UP_DEVCAP_MAX_PDU' => '131072')
48
+ mobile = Rack::Ketai::Carrier::Au.new(env)
49
+ mobile.cache_size.should == 131072
50
+ end
51
+
52
+ #
53
+ it "環境変数で取得できない古い機種のときは8220Byteにしとく(適当)" do
54
+ env = Rack::MockRequest.env_for('http://hoge.com/dummy',
55
+ 'HTTP_USER_AGENT' => 'UP.Browser/3.04-SYT4 UP.Link/3.4.5.6')
56
+ mobile = Rack::Ketai::Carrier::Au.new(env)
57
+ mobile.cache_size.should == 8220
58
+ end
59
+
60
+ end
61
+
62
+ describe "ディスプレイ情報を取得するとき" do
63
+
64
+ describe "既知の端末のとき" do
65
+
66
+ it "環境変数を優先すること" do
67
+ @env = Rack::MockRequest.env_for('http://hoge.com/dummy',
68
+ 'HTTP_USER_AGENT' => 'KDDI-TS31 UP.Browser/6.2.0.8 (GUI) MMP/2.0',
69
+ 'HTTP_X_UP_DEVCAP_SCREENPIXELS' => '1024,768',
70
+ 'HTTP_X_UP_DEVCAP_SCREENDEPTH' => '8')
71
+ @mobile = Rack::Ketai::Carrier::Au.new(@env)
72
+ display = @mobile.display
73
+ display.should_not be_nil
74
+ display.colors.should == 256
75
+ display.width.should == 1024
76
+ display.height.should == 768
77
+
78
+ @env = Rack::MockRequest.env_for('http://hoge.com/dummy',
79
+ 'HTTP_USER_AGENT' => 'KDDI-TS31 UP.Browser/6.2.0.8 (GUI) MMP/2.0')
80
+ @mobile = Rack::Ketai::Carrier::Au.new(@env)
81
+ display = @mobile.display
82
+ display.should_not be_nil
83
+ display.colors.should == 65536
84
+ display.width.should == 229
85
+ display.height.should == 270
86
+ end
87
+
88
+ end
89
+
90
+ describe "未知の端末のとき" do
91
+
92
+ it "環境変数から設定すること" do
93
+ @env = Rack::MockRequest.env_for('http://hoge.com/dummy',
94
+ 'HTTP_USER_AGENT' => 'KDDI-XX01 UP.Browser/6.2.0.8 (GUI) MMP/2.0',
95
+ 'HTTP_X_UP_DEVCAP_SCREENPIXELS' => '1024,768',
96
+ 'HTTP_X_UP_DEVCAP_SCREENDEPTH' => '8')
97
+ @mobile = Rack::Ketai::Carrier::Au.new(@env)
98
+ display = @mobile.display
99
+ display.should_not be_nil
100
+ display.colors.should == 256
101
+ display.width.should == 1024
102
+ display.height.should == 768
103
+ end
104
+
105
+ it "環境変数が無かったら慌てず騒がず nil を返す" do
106
+ @env = Rack::MockRequest.env_for('http://hoge.com/dummy',
107
+ 'HTTP_USER_AGENT' => 'KDDI-XX01 UP.Browser/6.2.0.8 (GUI) MMP/2.0')
108
+ @mobile = Rack::Ketai::Carrier::Au.new(@env)
109
+ display = @mobile.display
110
+ display.should_not be_nil
111
+ display.colors.should be_nil
112
+ display.width.should be_nil
113
+ display.height.should be_nil
114
+ end
115
+
116
+ end
117
+
118
+ end
119
+
120
+ describe "EZ番号が取得できないとき" do
121
+
122
+ before(:each) do
123
+ @env = Rack::MockRequest.env_for('http://hoge.com/dummy',
124
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0')
125
+ @mobile = Rack::Ketai::Carrier::Au.new(@env)
126
+ end
127
+
128
+ it "#subscriberid は nil を返すこと" do
129
+ @mobile.subscriberid.should be_nil
130
+ end
131
+
132
+ it "#deviceid は nil を返すこと" do
133
+ @mobile.deviceid.should be_nil
134
+ end
135
+
136
+ it "#ident は nil を返すこと" do
137
+ @mobile.ident.should be_nil
138
+ end
139
+ end
140
+
141
+ end
142
+
143
+ describe "#supports_cookie? を使うとき" do
144
+
145
+ # Au のCookie対応状況
146
+ # 全機種対応(GW側で保持)
147
+ # SSL接続時はWAP2.0ブラウザ搭載端末でのみ端末に保持したCookieを送出
148
+ # http://www.au.kddi.com/ezfactory/tec/spec/cookie.html
149
+
150
+ it "WAP1.0端末(HTTP)のとき true を返すこと" do
151
+ {
152
+ 'http://hoge.com/dummy' => {
153
+ 'HTTP_USER_AGENT' => 'UP.Browser/3.04-KCTE UP.Link/3.4.5.9'
154
+ },
155
+ 'http://hoge.com/dummy' => {
156
+ 'HTTP_USER_AGENT' => 'UP.Browser/3.04-KCTE UP.Link/3.4.5.9',
157
+ 'HTTPS' => 'OFF'
158
+ },
159
+ 'http://hoge.com/dummy' => {
160
+ 'HTTP_USER_AGENT' => 'UP.Browser/3.04-KCTE UP.Link/3.4.5.9',
161
+ 'X_FORWARDED_PROTO' => 'http'
162
+ }
163
+ }.each do |url, opt|
164
+ env = Rack::MockRequest.env_for(url, opt)
165
+ mobile = Rack::Ketai::Carrier::Au.new(env)
166
+ mobile.should be_respond_to(:supports_cookie?)
167
+ mobile.should be_supports_cookie
168
+ end
169
+
170
+ end
171
+
172
+ it "WAP1.0端末(HTTPS)のとき false を返すこと" do
173
+ {
174
+ 'https://hoge.com/dummy' => {
175
+ 'HTTP_USER_AGENT' => 'UP.Browser/3.04-KCTE UP.Link/3.4.5.9',
176
+ 'HTTPS' => 'on'
177
+ },
178
+ 'https://hoge.com/dummy' => {
179
+ 'HTTP_USER_AGENT' => 'UP.Browser/3.04-KCTE UP.Link/3.4.5.9',
180
+ 'HTTPS' => 'ON'
181
+ },
182
+ 'http://hoge.com/dummy' => {
183
+ 'HTTP_USER_AGENT' => 'UP.Browser/3.04-KCTE UP.Link/3.4.5.9',
184
+ 'X_FORWARDED_PROTO' => 'https' # RAILS的 リバースプロキシのバックエンドでHTTPSを判断する方法
185
+ }
186
+ }.each do |url, opt|
187
+ env = Rack::MockRequest.env_for(url, opt)
188
+ mobile = Rack::Ketai::Carrier::Au.new(env)
189
+ mobile.should be_respond_to(:supports_cookie?)
190
+ mobile.should_not be_supports_cookie
191
+ end
192
+ end
193
+
194
+ it "WAP2.0端末(HTTP)のとき true を返すこと" do
195
+ {
196
+ 'http://hoge.com/dummy' => {
197
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0'
198
+ },
199
+ 'http://hoge.com/dummy' => {
200
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0',
201
+ 'HTTPS' => 'OFF'
202
+ },
203
+ 'http://hoge.com/dummy' => {
204
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0',
205
+ 'X_FORWARDED_PROTO' => 'http'
206
+ }
207
+ }.each do |url, opt|
208
+ env = Rack::MockRequest.env_for(url, opt)
209
+ mobile = Rack::Ketai::Carrier::Au.new(env)
210
+ mobile.should be_respond_to(:supports_cookie?)
211
+ mobile.should be_supports_cookie
212
+ end
213
+
214
+ end
215
+
216
+ it "WAP2.0端末(HTTPS)のとき true を返すこと" do
217
+ {
218
+ 'https://hoge.com/dummy' => {
219
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0',
220
+ 'HTTPS' => 'on'
221
+ },
222
+ 'https://hoge.com/dummy' => {
223
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0',
224
+ 'HTTPS' => 'ON'
225
+ },
226
+ 'http://hoge.com/dummy' => {
227
+ 'HTTP_USER_AGENT' => 'KDDI-SA31 UP.Browser/6.2.0.7.3.129 (GUI) MMP/2.0',
228
+ 'X_FORWARDED_PROTO' => 'https' # RAILS的 リバースプロキシのバックエンドでHTTPSを判断する方法
229
+ }
230
+ }.each do |url, opt|
231
+ env = Rack::MockRequest.env_for(url, opt)
232
+ mobile = Rack::Ketai::Carrier::Au.new(env)
233
+ mobile.should be_respond_to(:supports_cookie?)
234
+ mobile.should be_supports_cookie
235
+ end
236
+ end
237
+
238
+ end
239
+
240
+ end
@@ -0,0 +1,30 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ describe Rack::Ketai::Carrier, "#load を実行するとき" do
4
+
5
+ it "適切なキャリアを設定できること" do
6
+ {
7
+ 'DoCoMo/1.0/N505i' => Rack::Ketai::Carrier::Docomo, # Mova
8
+ 'DoCoMo/2.0 P903i' => Rack::Ketai::Carrier::Docomo, # FOMA
9
+ 'KDDI-CA39 UP.Browser/6.2.0.13.1.5 (GUI) MMP/2.0' => Rack::Ketai::Carrier::Au, # WAP2.0 MMP2.0
10
+ 'KDDI-TS21 UP.Browser/6.0.2.273 (GUI) MMP/1.1' => Rack::Ketai::Carrier::Au, # WAP2.0 MMP1.1
11
+ 'SoftBank/1.0/930SH/SHJ001[/Serial] Browser/NetFront/3.4 Profile/MIDP-2.0 Configuration/CLDC-1.1' => Rack::Ketai::Carrier::Softbank, # SoftBank 3GC
12
+ 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 2_0_1 like Mac OS X; ja-jp) AppleWebKit/525.18.1 (KHTML, like Gecko) Version/3.1.1 Mobile/5B108 Safari/525.20' => Rack::Ketai::Carrier::IPhone,
13
+ 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 5.1; Trident/4.0; Q312461; .NET CLR 1.0.3705; .NET CLR 1.1.4322; .NET CLR 2.0.50727)' => nil, # IE8
14
+ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; ja; rv:1.9.0.1) Gecko/2008070208 Firefox/3.0.1' => nil,
15
+ 'Opera/9.21 (Windows NT 5.1; U; ja)' => nil,
16
+ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; ja-JP) AppleWebKit/525.19 (KHTML, like Gecko) Version/3.1.2 Safari/525.21' => nil,
17
+ 'Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US) AppleWebKit/525.13 (KHTML, like Gecko) Chrome/0.2.149.29 Safari/525.13' => nil,
18
+ }.each do |ua, carrier|
19
+ env = Rack::MockRequest.env_for('http://hoge.com/dummy','HTTP_USER_AGENT' => ua)
20
+ obj = Rack::Ketai::Carrier.load(env)
21
+ if carrier
22
+ obj.should be_is_a(carrier)
23
+ obj.should be_mobile
24
+ else
25
+ obj.should_not be_mobile
26
+ end
27
+ end
28
+ end
29
+
30
+ end
@@ -0,0 +1,25 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'rack/ketai/display'
3
+ describe "Rack::Ketai::Display" do
4
+
5
+ before(:each) do
6
+ @display1 = Rack::Ketai::Display.new
7
+ @display2 = Rack::Ketai::Display.new(:colors => 256, :width => 240, :height => 360)
8
+ end
9
+
10
+ it "#colors で色数を取得可能なこと、未設定ならnil" do
11
+ @display1.colors.should be_nil
12
+ @display2.colors.should == 256
13
+ end
14
+
15
+ it "#width でブラウザ横幅を取得可能なこと、未設定ならnil" do
16
+ @display1.width.should be_nil
17
+ @display2.width.should == 240
18
+ end
19
+
20
+ it "#height でブラウザ縦幅を取得可能なこと、未設定ならnil" do
21
+ @display1.height.should be_nil
22
+ @display2.height.should == 360
23
+ end
24
+
25
+ end
@@ -0,0 +1,106 @@
1
+ # -*- coding: utf-8 -*-
2
+ require 'kconv'
3
+ require 'rack/ketai/carrier/docomo'
4
+ describe Rack::Ketai::Carrier::Docomo::Filter, "内部エンコーディングに変換する時" do
5
+
6
+ before(:each) do
7
+ @filter = Rack::Ketai::Carrier::Docomo::Filter.new
8
+ end
9
+
10
+ it "POSTデータ中のSJISバイナリの絵文字を絵文字IDに変換すること" do
11
+ Rack::Ketai::Carrier::Docomo::Filter::EMOJI_TO_EMOJIID.should_not be_empty
12
+ Rack::Ketai::Carrier::Docomo::Filter::EMOJI_TO_EMOJIID.each do |emoji, emojiid|
13
+ postdata = CGI.escape("message=今日はいい".tosjis + emoji + "ですね。".tosjis)
14
+ postdata.force_encoding('Shift_JIS') if postdata.respond_to?(:force_encoding)
15
+
16
+ env = Rack::MockRequest.env_for('http://hoge.com/dummy',
17
+ 'HTTP_USER_AGENT' => 'DoCoMo/2.0 P903i',
18
+ :method => 'POST', # rack 1.1.0 以降ではこれがないとパーサが動かない
19
+ :input => postdata)
20
+ env = @filter.inbound(env)
21
+ request = Rack::Request.new(env)
22
+ request.params['message'].should == '今日はいい[e:'+format("%03X", emojiid)+']ですね。'
23
+ end
24
+ end
25
+
26
+ end
27
+
28
+ describe Rack::Ketai::Carrier::Docomo::Filter, "外部エンコーディングに変換する時" do
29
+
30
+ before(:each) do
31
+ @filter = Rack::Ketai::Carrier::Docomo::Filter.new
32
+ end
33
+
34
+ it "データ中の絵文字IDをSJISの絵文字コードに変換すること" do
35
+ Rack::Ketai::Carrier::Docomo::Filter::EMOJI_TO_EMOJIID.should_not be_empty
36
+ Rack::Ketai::Carrier::Docomo::Filter::EMOJI_TO_EMOJIID.each do |emoji, emojiid|
37
+ resdata = "今日はいい".tosjis + emoji + "ですね。".tosjis
38
+
39
+ status, headers, body = @filter.outbound(200, { "Content-Type" => "text/html"}, ['今日はいい[e:'+format("%03X", emojiid)+']ですね。'])
40
+
41
+ body[0].should == resdata
42
+ end
43
+
44
+ # 複数の絵文字IDに割り当てられている絵文字
45
+ hotel = [0xF8CA].pack('n*')
46
+ harts = [0xF994].pack('n*')
47
+ [hotel, harts].each{ |e| e.force_encoding('Shift_JIS') if e.respond_to?(:force_encoding) }
48
+ resdata = "ラブホテル".tosjis + hotel + harts
49
+ status, headers, body = @filter.outbound(200, { "Content-Type" => "text/html"}, ['ラブホテル[e:4B8]'])
50
+
51
+ body[0].should == resdata
52
+
53
+ end
54
+
55
+ it "Content-typeが指定なし,text/html, application/xhtml+xml 以外の時はフィルタを適用しないこと" do
56
+ Rack::Ketai::Carrier::Docomo::Filter::EMOJI_TO_EMOJIID.should_not be_empty
57
+ Rack::Ketai::Carrier::Docomo::Filter::EMOJI_TO_EMOJIID.each do |emoji, emojiid|
58
+ internaldata = '今日はいい[e:'+format("%03X", emojiid)+']ですね。'
59
+ %w(text/plain text/xml text/json application/json text/javascript application/rss+xml image/jpeg).each do |contenttype|
60
+ status, headers, body = @filter.outbound(200, { "Content-Type" => contenttype }, [internaldata])
61
+ body[0].should == internaldata
62
+ end
63
+ end
64
+ end
65
+
66
+ it "データ中に絵文字ID=絵文字IDだが絵文字!=絵文字IDのIDが含まれているとき、正しく逆変換できること" do
67
+ emoji = [0xF995].pack('n')
68
+ emoji.force_encoding('Shift_JIS') if emoji.respond_to?(:force_encoding)
69
+ resdata = "たとえば".tosjis+emoji+"「e-330 HAPPY FACE WITH OPEN MOUTH」とか。".tosjis
70
+
71
+ status, headers, body = @filter.outbound(200, { "Content-Type" => "text/html"}, ["たとえば[e:330]「e-330 HAPPY FACE WITH OPEN MOUTH」とか。"])
72
+
73
+ body[0].should == resdata
74
+ end
75
+
76
+ it "データ中にドコモにはない絵文字IDが存在するとき、代替文字を表示すること" do
77
+ resdata = "黒い矢印[#{[0x2190].pack('U')}]です".tosjis # 左黒矢印
78
+
79
+ status, headers, body = @filter.outbound(200, { "Content-Type" => "text/html"}, ['黒い矢印[e:AFB]です'])
80
+
81
+ body[0].should == resdata
82
+ end
83
+
84
+ it "Content-typeを適切に書き換えられること" do
85
+ [
86
+ [nil, nil],
87
+ ['text/html', 'application/xhtml+xml; charset=shift_jis'],
88
+ ['text/html; charset=utf-8', 'application/xhtml+xml; charset=shift_jis'],
89
+ ['text/html;charset=utf-8', 'application/xhtml+xml;charset=shift_jis'],
90
+ ['application/xhtml+xml', 'application/xhtml+xml; charset=shift_jis'],
91
+ ['application/xhtml+xml; charset=utf-8', 'application/xhtml+xml; charset=shift_jis'],
92
+ ['application/xhtml+xml;charset=utf-8', 'application/xhtml+xml;charset=shift_jis'],
93
+ ['text/javascript', 'text/javascript'],
94
+ ['text/json', 'text/json'],
95
+ ['application/json', 'application/json'],
96
+ ['text/javascript+json', 'text/javascript+json'],
97
+ ['image/jpeg', 'image/jpeg'],
98
+ ['application/octet-stream', 'application/octet-stream'],
99
+ ].each do |content_type, valid_content_type|
100
+ status, headers, body = @filter.outbound(200, { "Content-Type" => content_type}, ['適当な本文'])
101
+ headers['Content-Type'].should == valid_content_type
102
+ end
103
+ end
104
+
105
+ end
106
+