jellyfish 1.0.2 → 1.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.travis.yml +1 -0
- data/CHANGES.md +14 -0
- data/README.md +194 -119
- data/bench/bench_builder.rb +44 -0
- data/config.ru +5 -39
- data/jellyfish.gemspec +12 -18
- data/lib/jellyfish.rb +1 -3
- data/lib/jellyfish/builder.rb +52 -0
- data/lib/jellyfish/test.rb +8 -8
- data/lib/jellyfish/urlmap.rb +26 -0
- data/lib/jellyfish/version.rb +1 -1
- data/task/gemgem.rb +11 -2
- data/test/rack/test_builder.rb +154 -0
- data/test/rack/test_urlmap.rb +180 -0
- data/test/sinatra/test_base.rb +1 -1
- data/test/sinatra/test_routing.rb +0 -60
- data/test/test_from_readme.rb +31 -20
- metadata +15 -17
- data/lib/jellyfish/multi_actions.rb +0 -31
- data/lib/jellyfish/sinatra.rb +0 -13
- data/lib/jellyfish/swagger.rb +0 -166
- data/public/css/screen.css +0 -1070
- data/public/index.html +0 -45
- data/public/js/shred.bundle.js +0 -2765
- data/public/js/shred/content.js +0 -193
- data/public/js/swagger-ui.js +0 -2116
- data/public/js/swagger.js +0 -1400
- data/test/sinatra/test_multi_actions.rb +0 -217
- data/test/test_swagger.rb +0 -131
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f177b766380820557e01f05b9da4efacc4895984
|
4
|
+
data.tar.gz: 800a25a7fa325e5c0c40c7604b94cbddb32c43a6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 288710b695398769a27f730e3a2d2d6fe921b140455a968f57515503628af9f7759506c89c9291b860e862b1eb6ca56d076b9ca01c2429d53626bc021d189904
|
7
|
+
data.tar.gz: 78edee35fbbac6dd332fc9eb9290b1dc26c27b74abd72c57166bb0d429ac6f3d441c3c88f56f014befdeb32825a47bf6d395dae04e21c624b6c52caa986aa589
|
data/.travis.yml
CHANGED
data/CHANGES.md
CHANGED
@@ -1,5 +1,19 @@
|
|
1
1
|
# CHANGES
|
2
2
|
|
3
|
+
## Jellyfish 1.1.0 -- 2015-09-25
|
4
|
+
|
5
|
+
### Incompatible changes
|
6
|
+
|
7
|
+
* `Jellyfish::Sinatra`, `Jellyfish::MultiActions`, and `Jellyfish::Swagger`
|
8
|
+
were extracted to
|
9
|
+
[jellyfish-contrib](https://github.com/godfat/jellyfish-contrib)
|
10
|
+
|
11
|
+
### Other enhancements
|
12
|
+
|
13
|
+
* Added `Jellyfish::Builder` and `Jellyfish::URLMap` which is 36 times faster
|
14
|
+
than `Rack::Builder` and `Rack::URLMap` given an application with
|
15
|
+
1000 routes.
|
16
|
+
|
3
17
|
## Jellyfish 1.0.2 -- 2014-12-09
|
4
18
|
|
5
19
|
* `Jellyfish::NewRelic` is fixed. Thanks Jason R. Clark (@jasonrclark)
|
data/README.md
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# Jellyfish [![Build Status](https://secure.travis-ci.org/godfat/jellyfish.png?branch=master)](http://travis-ci.org/godfat/jellyfish) [![Coverage Status](https://coveralls.io/repos/godfat/jellyfish/badge.png)](https://coveralls.io/r/godfat/jellyfish)
|
1
|
+
# Jellyfish [![Build Status](https://secure.travis-ci.org/godfat/jellyfish.png?branch=master)](http://travis-ci.org/godfat/jellyfish) [![Coverage Status](https://coveralls.io/repos/godfat/jellyfish/badge.png)](https://coveralls.io/r/godfat/jellyfish) [![Join the chat at https://gitter.im/godfat/jellyfish](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/godfat/jellyfish)
|
2
2
|
|
3
3
|
by Lin Jen-Shin ([godfat](http://godfat.org))
|
4
4
|
|
@@ -13,7 +13,11 @@ by Lin Jen-Shin ([godfat](http://godfat.org))
|
|
13
13
|
## DESCRIPTION:
|
14
14
|
|
15
15
|
Pico web framework for building API-centric web applications.
|
16
|
-
For Rack applications or Rack
|
16
|
+
For Rack applications or Rack middleware. Around 250 lines of code.
|
17
|
+
|
18
|
+
Check [jellyfish-contrib][] for extra extensions.
|
19
|
+
|
20
|
+
[jellyfish-contrib]: https://github.com/godfat/jellyfish-contrib
|
17
21
|
|
18
22
|
## DESIGN:
|
19
23
|
|
@@ -43,7 +47,7 @@ For Rack applications or Rack middlewares. Around 250 lines of code.
|
|
43
47
|
* String routes, e.g. `get '/'`
|
44
48
|
* Custom routes, e.g. `get Matcher.new`
|
45
49
|
* Build for either Rack applications or Rack middleware
|
46
|
-
* Include extensions for more features (
|
50
|
+
* Include extensions for more features (checkout [jellyfish-contrib][])
|
47
51
|
|
48
52
|
## WHY?
|
49
53
|
|
@@ -59,9 +63,7 @@ Because Sinatra is too complex and inconsistent for me.
|
|
59
63
|
|
60
64
|
## SYNOPSIS:
|
61
65
|
|
62
|
-
You could also take a look at [config.ru](config.ru) as an example
|
63
|
-
also uses [Swagger](https://helloreverb.com/developers/swagger) to generate
|
64
|
-
API documentation.
|
66
|
+
You could also take a look at [config.ru](config.ru) as an example.
|
65
67
|
|
66
68
|
### Hello Jellyfish, your lovely config.ru
|
67
69
|
|
@@ -439,33 +441,148 @@ GET /123
|
|
439
441
|
["Jelly jumps.\n"]]
|
440
442
|
-->
|
441
443
|
|
442
|
-
### Extension:
|
444
|
+
### Extension: Jellyfish::Builder, a faster Rack::Builder and Rack::URLMap
|
445
|
+
|
446
|
+
Default `Rack::Builder` and `Rack::URLMap` is routing via linear search,
|
447
|
+
which could be very slow with a large number of routes. We could use
|
448
|
+
`Jellyfish::Builder` in this case because it would compile the routes
|
449
|
+
into a regular expression, it would be matching much faster than
|
450
|
+
linear search.
|
451
|
+
|
452
|
+
Note that `Jellyfish::Builder` is not a complete compatible implementation.
|
453
|
+
The followings are intentional:
|
454
|
+
|
455
|
+
* There's no `Jellyfish::Builder.call` because it doesn't make sense in my
|
456
|
+
opinion. Always use `Jellyfish::Builder.app` instead.
|
457
|
+
|
458
|
+
* There's no `Jellyfish::Builder.parse_file` and
|
459
|
+
`Jellyfish::Builder.new_from_string` because Rack servers are not
|
460
|
+
going to use `Jellyfish::Builder` to parse `config.ru` at this point.
|
461
|
+
We could provide this if there's a need.
|
462
|
+
|
463
|
+
* `Jellyfish::URLMap` does not modify `env`, and it would call the app with
|
464
|
+
another instance of Hash. Mutating data is a bad idea.
|
465
|
+
|
466
|
+
* `Jellyfish::URLMap` does not try to match on host because I am not sure
|
467
|
+
if there's anyone would need this feature?
|
468
|
+
|
469
|
+
* All other tests passed the same test suites for `Rack::Builder`.
|
443
470
|
|
444
471
|
``` ruby
|
445
472
|
require 'jellyfish'
|
446
|
-
class Tank
|
447
|
-
include Jellyfish
|
448
|
-
controller_include Jellyfish::MultiActions
|
449
473
|
|
450
|
-
|
451
|
-
|
474
|
+
run Jellyfish::Builder.app{
|
475
|
+
map '/a' do; run lambda{ |_| [200, {}, ["a\n"] ] }; end
|
476
|
+
map '/b' do; run lambda{ |_| [200, {}, ["b\n"] ] }; end
|
477
|
+
map '/c' do; run lambda{ |_| [200, {}, ["c\n"] ] }; end
|
478
|
+
map '/d' do; run lambda{ |_| [200, {}, ["d\n"] ] }; end
|
479
|
+
map '/e' do
|
480
|
+
map '/f' do; run lambda{ |_| [200, {}, ["e/f\n"]] }; end
|
481
|
+
map '/g' do; run lambda{ |_| [200, {}, ["e/g\n"]] }; end
|
482
|
+
map '/h' do; run lambda{ |_| [200, {}, ["e/h\n"]] }; end
|
483
|
+
map '/i' do; run lambda{ |_| [200, {}, ["e/i\n"]] }; end
|
484
|
+
map '/' do; run lambda{ |_| [200, {}, ["e\n"]] }; end
|
452
485
|
end
|
453
|
-
|
454
|
-
|
486
|
+
map '/j' do; run lambda{ |_| [200, {}, ["j\n"] ] }; end
|
487
|
+
map '/k' do; run lambda{ |_| [200, {}, ["k\n"] ] }; end
|
488
|
+
map '/l' do; run lambda{ |_| [200, {}, ["l\n"] ] }; end
|
489
|
+
map '/m' do
|
490
|
+
map '/g' do; run lambda{ |_| [200, {}, ["m/g\n"]] }; end
|
491
|
+
run lambda{ |_| [200, {}, ["m\n"] ] }
|
455
492
|
end
|
456
|
-
|
457
|
-
use Rack::ContentLength
|
458
|
-
|
459
|
-
|
493
|
+
|
494
|
+
use Rack::ContentLength
|
495
|
+
run lambda{ |_| [200, {}, ["/\n"]] }
|
496
|
+
}
|
460
497
|
```
|
461
498
|
|
462
499
|
<!---
|
463
|
-
GET /
|
464
|
-
[200,
|
465
|
-
|
466
|
-
|
500
|
+
GET /a
|
501
|
+
[200, {}, ["a\n"]]
|
502
|
+
|
503
|
+
GET /a/x
|
504
|
+
[200, {}, ["a\n"]]
|
505
|
+
|
506
|
+
GET /b
|
507
|
+
[200, {}, ["b\n"]]
|
508
|
+
|
509
|
+
GET /c
|
510
|
+
[200, {}, ["c\n"]]
|
511
|
+
|
512
|
+
GET /d
|
513
|
+
[200, {}, ["d\n"]]
|
514
|
+
|
515
|
+
GET /e/f
|
516
|
+
[200, {}, ["e/f\n"]]
|
517
|
+
|
518
|
+
GET /e/g
|
519
|
+
[200, {}, ["e/g\n"]]
|
520
|
+
|
521
|
+
GET /e/h
|
522
|
+
[200, {}, ["e/h\n"]]
|
523
|
+
|
524
|
+
GET /e/i
|
525
|
+
[200, {}, ["e/i\n"]]
|
526
|
+
|
527
|
+
GET /e/
|
528
|
+
[200, {}, ["e\n"]]
|
529
|
+
|
530
|
+
GET /e
|
531
|
+
[200, {}, ["e\n"]]
|
532
|
+
|
533
|
+
GET /e/x
|
534
|
+
[200, {}, ["e\n"]]
|
535
|
+
|
536
|
+
GET /j
|
537
|
+
[200, {}, ["j\n"]]
|
538
|
+
|
539
|
+
GET /k
|
540
|
+
[200, {}, ["k\n"]]
|
541
|
+
|
542
|
+
GET /l
|
543
|
+
[200, {}, ["l\n"]]
|
544
|
+
|
545
|
+
GET /m/g
|
546
|
+
[200, {}, ["m/g\n"]]
|
547
|
+
|
548
|
+
GET /m
|
549
|
+
[200, {}, ["m\n"]]
|
550
|
+
|
551
|
+
GET /m/
|
552
|
+
[200, {}, ["m\n"]]
|
553
|
+
|
554
|
+
GET /m/x
|
555
|
+
[200, {}, ["m\n"]]
|
556
|
+
|
557
|
+
GET /
|
558
|
+
[200, {'Content-Length' => '2'}, ["/\n"]]
|
559
|
+
|
560
|
+
GET /x
|
561
|
+
[200, {'Content-Length' => '2'}, ["/\n"]]
|
562
|
+
|
563
|
+
GET /ab
|
564
|
+
[200, {'Content-Length' => '2'}, ["/\n"]]
|
467
565
|
-->
|
468
566
|
|
567
|
+
You could try a stupid benchmark yourself:
|
568
|
+
|
569
|
+
ruby -Ilib bench/bench_builder.rb
|
570
|
+
|
571
|
+
For a 1000 routes app, here's my result:
|
572
|
+
|
573
|
+
```
|
574
|
+
Calculating -------------------------------------
|
575
|
+
Jellyfish::URLMap 5.726k i/100ms
|
576
|
+
Rack::URLMap 167.000 i/100ms
|
577
|
+
-------------------------------------------------
|
578
|
+
Jellyfish::URLMap 62.397k (± 1.2%) i/s - 314.930k
|
579
|
+
Rack::URLMap 1.702k (± 1.5%) i/s - 8.517k
|
580
|
+
|
581
|
+
Comparison:
|
582
|
+
Jellyfish::URLMap: 62397.3 i/s
|
583
|
+
Rack::URLMap: 1702.0 i/s - 36.66x slower
|
584
|
+
```
|
585
|
+
|
469
586
|
### Extension: NormalizedParams (with force_encoding)
|
470
587
|
|
471
588
|
``` ruby
|
@@ -514,39 +631,6 @@ GET /%E5%9B%A7
|
|
514
631
|
["/%E5%9B%A7=/\u{56e7}\n"]]
|
515
632
|
-->
|
516
633
|
|
517
|
-
### Extension: Sinatra flavoured controller
|
518
|
-
|
519
|
-
It's an extension collection contains:
|
520
|
-
|
521
|
-
* MultiActions
|
522
|
-
* NormalizedParams
|
523
|
-
* NormalizedPath
|
524
|
-
|
525
|
-
``` ruby
|
526
|
-
require 'jellyfish'
|
527
|
-
class Tank
|
528
|
-
include Jellyfish
|
529
|
-
controller_include Jellyfish::Sinatra
|
530
|
-
|
531
|
-
get do # wildcard before filter
|
532
|
-
@state = 'jumps'
|
533
|
-
end
|
534
|
-
get %r{^/(?<id>\d+)$} do
|
535
|
-
"Jelly ##{params[:id]} #{@state}.\n"
|
536
|
-
end
|
537
|
-
end
|
538
|
-
use Rack::ContentLength
|
539
|
-
use Rack::ContentType, 'text/plain'
|
540
|
-
run Tank.new
|
541
|
-
```
|
542
|
-
|
543
|
-
<!---
|
544
|
-
GET /123
|
545
|
-
[200,
|
546
|
-
{'Content-Length' => '18', 'Content-Type' => 'text/plain'},
|
547
|
-
["Jelly #123 jumps.\n"]]
|
548
|
-
-->
|
549
|
-
|
550
634
|
### Extension: NewRelic
|
551
635
|
|
552
636
|
``` ruby
|
@@ -577,7 +661,6 @@ GET /
|
|
577
661
|
|
578
662
|
### Extension: Using multiple extensions with custom controller
|
579
663
|
|
580
|
-
This is effectively the same as using Jellyfish::Sinatra extension.
|
581
664
|
Note that the controller should be assigned lastly in order to include
|
582
665
|
modules remembered in controller_include.
|
583
666
|
|
@@ -586,16 +669,13 @@ require 'jellyfish'
|
|
586
669
|
class Tank
|
587
670
|
include Jellyfish
|
588
671
|
class MyController < Jellyfish::Controller
|
589
|
-
include Jellyfish::
|
672
|
+
include Jellyfish::WebSocket
|
590
673
|
end
|
591
674
|
controller_include NormalizedParams, NormalizedPath
|
592
675
|
controller MyController
|
593
676
|
|
594
|
-
get do # wildcard before filter
|
595
|
-
@state = 'jumps'
|
596
|
-
end
|
597
677
|
get %r{^/(?<id>\d+)$} do
|
598
|
-
"Jelly ##{params[:id]}
|
678
|
+
"Jelly ##{params[:id]} jumps.\n"
|
599
679
|
end
|
600
680
|
end
|
601
681
|
use Rack::ContentLength
|
@@ -761,35 +841,6 @@ GET /status
|
|
761
841
|
["30\u{2103}\n"]]
|
762
842
|
-->
|
763
843
|
|
764
|
-
### Halt in before action
|
765
|
-
|
766
|
-
``` ruby
|
767
|
-
require 'jellyfish'
|
768
|
-
class Tank
|
769
|
-
include Jellyfish
|
770
|
-
controller_include Jellyfish::MultiActions
|
771
|
-
|
772
|
-
get do # wildcard before filter
|
773
|
-
body "Done!\n"
|
774
|
-
halt
|
775
|
-
end
|
776
|
-
get '/' do
|
777
|
-
"Never reach.\n"
|
778
|
-
end
|
779
|
-
end
|
780
|
-
|
781
|
-
use Rack::ContentLength
|
782
|
-
use Rack::ContentType, 'text/plain'
|
783
|
-
run Tank.new
|
784
|
-
```
|
785
|
-
|
786
|
-
<!---
|
787
|
-
GET /status
|
788
|
-
[200,
|
789
|
-
{'Content-Length' => '6', 'Content-Type' => 'text/plain'},
|
790
|
-
["Done!\n"]]
|
791
|
-
-->
|
792
|
-
|
793
844
|
### One huge tank
|
794
845
|
|
795
846
|
``` ruby
|
@@ -912,6 +963,57 @@ GET /chunked
|
|
912
963
|
"2\r\n3\n\r\n", "2\r\n4\n\r\n", "0\r\n\r\n"]]
|
913
964
|
-->
|
914
965
|
|
966
|
+
### Server Sent Event (SSE)
|
967
|
+
|
968
|
+
``` ruby
|
969
|
+
class Tank
|
970
|
+
include Jellyfish
|
971
|
+
class Body
|
972
|
+
def each
|
973
|
+
(0..4).each{ |i| yield "data: #{i}\n\n" }
|
974
|
+
end
|
975
|
+
end
|
976
|
+
get '/sse' do
|
977
|
+
headers_merge('Content-Type' => 'text/event-stream')
|
978
|
+
Body.new
|
979
|
+
end
|
980
|
+
end
|
981
|
+
run Tank.new
|
982
|
+
```
|
983
|
+
|
984
|
+
<!---
|
985
|
+
GET /sse
|
986
|
+
[200,
|
987
|
+
{'Content-Type' => 'text/event-stream'},
|
988
|
+
["data: 0\n\n", "data: 1\n\n", "data: 2\n\n", "data: 3\n\n", "data: 4\n\n"]]
|
989
|
+
-->
|
990
|
+
|
991
|
+
### Server Sent Event (SSE) with Rack Hijacking
|
992
|
+
|
993
|
+
``` ruby
|
994
|
+
class Tank
|
995
|
+
include Jellyfish
|
996
|
+
get '/sse' do
|
997
|
+
headers_merge(
|
998
|
+
'Content-Type' => 'text/event-stream',
|
999
|
+
'rack.hijack' => lambda do |sock|
|
1000
|
+
(0..4).each do |i|
|
1001
|
+
sock.write("data: #{i}\n\n")
|
1002
|
+
end
|
1003
|
+
sock.close
|
1004
|
+
end)
|
1005
|
+
end
|
1006
|
+
end
|
1007
|
+
run Tank.new
|
1008
|
+
```
|
1009
|
+
|
1010
|
+
<!---
|
1011
|
+
GET /sse
|
1012
|
+
[200,
|
1013
|
+
{'Content-Type' => 'text/event-stream'},
|
1014
|
+
["data: 0\n\n", "data: 1\n\n", "data: 2\n\n", "data: 3\n\n", "data: 4\n\n"]]
|
1015
|
+
-->
|
1016
|
+
|
915
1017
|
### Using WebSocket
|
916
1018
|
|
917
1019
|
Note that this only works for Rack servers which support [hijack][].
|
@@ -956,36 +1058,9 @@ HTTP
|
|
956
1058
|
[200, {}, ['']]
|
957
1059
|
-->
|
958
1060
|
|
959
|
-
### Use Swagger to generate API documentation
|
960
|
-
|
961
|
-
For a complete example, checkout [config.ru](config.ru).
|
962
|
-
|
963
|
-
``` ruby
|
964
|
-
require 'jellyfish'
|
965
|
-
class Tank
|
966
|
-
include Jellyfish
|
967
|
-
get %r{^/(?<id>\d+)$}, :notes => 'This is an API note' do |match|
|
968
|
-
"Jelly ##{match[:id]}\n"
|
969
|
-
end
|
970
|
-
end
|
971
|
-
use Rack::ContentLength
|
972
|
-
use Rack::ContentType, 'text/plain'
|
973
|
-
map '/swagger' do
|
974
|
-
run Jellyfish::Swagger.new('', Tank)
|
975
|
-
end
|
976
|
-
run Tank.new
|
977
|
-
```
|
978
|
-
|
979
|
-
<!---
|
980
|
-
GET /swagger
|
981
|
-
[200,
|
982
|
-
{'Content-Type' => 'application/json; charset=utf-8',
|
983
|
-
'Content-Length' => '81'},
|
984
|
-
['{"swaggerVersion":"1.2","info":{},"apiVersion":"0.1.0","apis":[{"path":"/{id}"}]}']]
|
985
|
-
-->
|
986
|
-
|
987
1061
|
## CONTRIBUTORS:
|
988
1062
|
|
1063
|
+
* Fumin (@fumin)
|
989
1064
|
* Jason R. Clark (@jasonrclark)
|
990
1065
|
* Lin Jen-Shin (@godfat)
|
991
1066
|
|
@@ -993,7 +1068,7 @@ GET /swagger
|
|
993
1068
|
|
994
1069
|
Apache License 2.0
|
995
1070
|
|
996
|
-
Copyright (c) 2012-
|
1071
|
+
Copyright (c) 2012-2015, Lin Jen-Shin (godfat)
|
997
1072
|
|
998
1073
|
Licensed under the Apache License, Version 2.0 (the "License");
|
999
1074
|
you may not use this file except in compliance with the License.
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
# Calculating -------------------------------------
|
3
|
+
# Jellyfish::URLMap 5.726k i/100ms
|
4
|
+
# Rack::URLMap 167.000 i/100ms
|
5
|
+
# -------------------------------------------------
|
6
|
+
# Jellyfish::URLMap 62.397k (± 1.2%) i/s - 314.930k
|
7
|
+
# Rack::URLMap 1.702k (± 1.5%) i/s - 8.517k
|
8
|
+
|
9
|
+
# Comparison:
|
10
|
+
# Jellyfish::URLMap: 62397.3 i/s
|
11
|
+
# Rack::URLMap: 1702.0 i/s - 36.66x slower
|
12
|
+
|
13
|
+
require 'jellyfish'
|
14
|
+
require 'rack'
|
15
|
+
|
16
|
+
require 'benchmark/ips'
|
17
|
+
|
18
|
+
num = 1000
|
19
|
+
app = lambda do |_|
|
20
|
+
ok = [200, {}, []]
|
21
|
+
rn = lambda{ |_| ok }
|
22
|
+
|
23
|
+
(0...num).each do |i|
|
24
|
+
map "/#{i}" do
|
25
|
+
run rn
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
jelly = Jellyfish::Builder.app(&app)
|
31
|
+
rack = Rack::Builder.app(&app)
|
32
|
+
path_info = 'PATH_INFO'
|
33
|
+
|
34
|
+
Benchmark.ips do |x|
|
35
|
+
x.report(jelly.class) do
|
36
|
+
jelly.call(path_info => rand(num).to_s)
|
37
|
+
end
|
38
|
+
|
39
|
+
x.report(rack.class) do
|
40
|
+
rack.call(path_info => rand(num).to_s)
|
41
|
+
end
|
42
|
+
|
43
|
+
x.compare!
|
44
|
+
end
|