diff --git a/.gitignore b/.gitignore index 96c135f3..9f48f142 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ Godeps/* !Godeps/Godeps.json +coverage.out +count.out diff --git a/.travis.yml b/.travis.yml index 98b43464..695f0b7e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,22 @@ language: go - +sudo: false go: - - 1.3 + - 1.4 + - 1.4.2 - tip + +script: + - go get golang.org/x/tools/cmd/cover + - go get github.com/mattn/goveralls + - go test -v -covermode=count -coverprofile=coverage.out + +after_success: + - goveralls -coverprofile=coverage.out -service=travis-ci -repotoken yFj7FrCeddvBzUaaCyG33jCLfWXeb93eA + +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/acc2c57482e94b44f557 + on_success: change # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: false # default: false diff --git a/AUTHORS.md b/AUTHORS.md index 47cb58a5..2feaf467 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -4,12 +4,18 @@ List of all the awesome people working to make Gin the best Web Framework in Go. ##gin 0.x series authors -**Lead Developer:** Manu Martinez-Almeida (@manucorporat) -**Staff:** -Javier Provecho (@javierprovecho) +**Maintainer:** Manu Martinez-Almeida (@manucorporat), Javier Provecho (@javierprovecho) People and companies, who have contributed, in alphabetical order. +**@858806258 (杰哥)** +- Fix typo in example + + +**@achedeuzot (Klemen Sever)** +- Fix newline debug printing + + **@adammck (Adam Mckaig)** - Add MIT license @@ -18,6 +24,10 @@ People and companies, who have contributed, in alphabetical order. - Typos in README +**@alexanderdidenko (Aleksandr Didenko)** +- Add support multipart/form-data + + **@alexandernyquist (Alexander Nyquist)** - Using template.Must to fix multiple return issue - ★ Added support for OPTIONS verb @@ -31,6 +41,14 @@ People and companies, who have contributed, in alphabetical order. - Added travis CI integration +**@andredublin (Andre Dublin)** +- Fix typo in comment + + +**@bredov (Ludwig Valda Vasquez)** +- Fix html templating in debug mode + + **@bluele (Jun Kimura)** - Fixes code examples in README @@ -41,20 +59,66 @@ People and companies, who have contributed, in alphabetical order. **@dickeyxxx (Jeff Dickey)** - Typos in README +- Add example about serving static files + + +**@donileo (Adonis)** +- Add NoMethod handler + + +**@dutchcoders (DutchCoders)** +- ★ Fix security bug that allows client to spoof ip +- Fix typo. r.HTMLTemplates -> SetHTMLTemplate + + +**@el3ctro- (Joshua Loper)** +- Fix typo in example + + +**@ethankan (Ethan Kan)** +- Unsigned integers in binding + + +**(Evgeny Persienko)** +- Validate sub structures + + +**@frankbille (Frank Bille)** +- Add support for HTTP Realm Auth **@fmd (Fareed Dudhia)** - Fix typo. SetHTTPTemplate -> SetHTMLTemplate +**@ironiridis (Christopher Harrington)** +- Remove old reference + + +**@jammie-stackhouse (Jamie Stackhouse)** +- Add more shortcuts for router methods + + **@jasonrhansen** - Fix spelling and grammar errors in documentation +**@JasonSoft (Jason Lee)** +- Fix typo in comment + + +**@joiggama (Ignacio Galindo)** +- Add utf-8 charset header on renders + + **@julienschmidt (Julien Schmidt)** - gofmt the code examples +**@kelcecil (Kel Cecil)** +- Fix readme typo + + **@kyledinh (Kyle Dinh)** - Adds RunTLS() @@ -63,20 +127,33 @@ People and companies, who have contributed, in alphabetical order. - Small fixes in README +**@loongmxbt (Saint Asky)** +- Fix typo in example + + **@lucas-clemente (Lucas Clemente)** - ★ work around path.Join removing trailing slashes from routes +**@mattn (Yasuhiro Matsumoto)** +- Improve color logger + + **@mdigger (Dmitry Sedykh)** - Fixes Form binding when content-type is x-www-form-urlencoded - No repeat call c.Writer.Status() in gin.Logger - Fixes Content-Type for json render +**@mirzac (Mirza Ceric)** +- Fix debug printing + + **@mopemope (Yutaka Matsubara)** - ★ Adds Godep support (Dependencies Manager) - Fix variadic parameter in the flexible render API - Fix Corrupted plain render +- Add Pluggable View Renderer Example **@msemenistyi (Mykyta Semenistyi)** @@ -96,6 +173,26 @@ People and companies, who have contributed, in alphabetical order. - Fix Port usage in README. +**@rayrod2030 (Ray Rodriguez)** +- Fix typo in example + + +**@rns** +- Fix typo in example + + +**@RobAWilkinson (Robert Wilkinson)** +- Add example of forms and params + + +**@rogierlommers (Rogier Lommers)** +- Add updated static serve example + + +**@se77en (Damon Zhao)** +- Improve color logging + + **@silasb (Silas Baronda)** - Fixing quotes in README @@ -104,5 +201,29 @@ People and companies, who have contributed, in alphabetical order. - Fixes some texts in README II +**@slimmy (Jimmy Pettersson)** +- Added messages for required bindings + + +**@smira (Andrey Smirnov)** +- Add support for ignored/unexported fields in binding + + +**@superalsrk (SRK.Lyu)** +- Update httprouter godeps + + +**@tebeka (Miki Tebeka)** +- Use net/http constants instead of numeric values + + +**@techjanitor** +- Update context.go reserved IPs + + +**@yosssi (Keiji Yoshida)** +- Fix link in README + + **@yuyabee** - Fixed README \ No newline at end of file diff --git a/BENCHMARKS.md b/BENCHMARKS.md new file mode 100644 index 00000000..181f75b3 --- /dev/null +++ b/BENCHMARKS.md @@ -0,0 +1,298 @@ +**Machine:** intel i7 ivy bridge quad-core. 8GB RAM. +**Date:** June 4th, 2015 +[https://github.com/gin-gonic/go-http-routing-benchmark](https://github.com/gin-gonic/go-http-routing-benchmark) + +``` +BenchmarkAce_Param 5000000 372 ns/op 32 B/op 1 allocs/op +BenchmarkBear_Param 1000000 1165 ns/op 424 B/op 5 allocs/op +BenchmarkBeego_Param 1000000 2440 ns/op 720 B/op 10 allocs/op +BenchmarkBone_Param 1000000 1067 ns/op 384 B/op 3 allocs/op +BenchmarkDenco_Param 5000000 240 ns/op 32 B/op 1 allocs/op +BenchmarkEcho_Param 10000000 130 ns/op 0 B/op 0 allocs/op +BenchmarkGin_Param 10000000 133 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_Param 1000000 1826 ns/op 656 B/op 9 allocs/op +BenchmarkGoji_Param 2000000 957 ns/op 336 B/op 2 allocs/op +BenchmarkGoJsonRest_Param 1000000 2021 ns/op 657 B/op 14 allocs/op +BenchmarkGoRestful_Param 200000 8825 ns/op 2496 B/op 31 allocs/op +BenchmarkGorillaMux_Param 500000 3340 ns/op 784 B/op 9 allocs/op +BenchmarkHttpRouter_Param 10000000 152 ns/op 32 B/op 1 allocs/op +BenchmarkHttpTreeMux_Param 2000000 717 ns/op 336 B/op 2 allocs/op +BenchmarkKocha_Param 3000000 423 ns/op 56 B/op 3 allocs/op +BenchmarkMacaron_Param 1000000 3410 ns/op 1104 B/op 11 allocs/op +BenchmarkMartini_Param 200000 7101 ns/op 1152 B/op 12 allocs/op +BenchmarkPat_Param 1000000 2040 ns/op 656 B/op 14 allocs/op +BenchmarkPossum_Param 1000000 2048 ns/op 624 B/op 7 allocs/op +BenchmarkR2router_Param 1000000 1144 ns/op 432 B/op 6 allocs/op +BenchmarkRevel_Param 200000 6725 ns/op 1672 B/op 28 allocs/op +BenchmarkRivet_Param 1000000 1121 ns/op 464 B/op 5 allocs/op +BenchmarkTango_Param 1000000 1479 ns/op 256 B/op 10 allocs/op +BenchmarkTigerTonic_Param 1000000 3393 ns/op 992 B/op 19 allocs/op +BenchmarkTraffic_Param 300000 5525 ns/op 1984 B/op 23 allocs/op +BenchmarkVulcan_Param 2000000 924 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_Param 1000000 1084 ns/op 368 B/op 3 allocs/op +BenchmarkAce_Param5 3000000 614 ns/op 160 B/op 1 allocs/op +BenchmarkBear_Param5 1000000 1617 ns/op 469 B/op 5 allocs/op +BenchmarkBeego_Param5 1000000 3373 ns/op 992 B/op 13 allocs/op +BenchmarkBone_Param5 1000000 1478 ns/op 432 B/op 3 allocs/op +BenchmarkDenco_Param5 3000000 570 ns/op 160 B/op 1 allocs/op +BenchmarkEcho_Param5 5000000 256 ns/op 0 B/op 0 allocs/op +BenchmarkGin_Param5 10000000 222 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_Param5 1000000 2789 ns/op 928 B/op 12 allocs/op +BenchmarkGoji_Param5 1000000 1287 ns/op 336 B/op 2 allocs/op +BenchmarkGoJsonRest_Param5 1000000 3670 ns/op 1105 B/op 17 allocs/op +BenchmarkGoRestful_Param5 200000 10756 ns/op 2672 B/op 31 allocs/op +BenchmarkGorillaMux_Param5 300000 5543 ns/op 912 B/op 9 allocs/op +BenchmarkHttpRouter_Param5 5000000 403 ns/op 160 B/op 1 allocs/op +BenchmarkHttpTreeMux_Param5 1000000 1089 ns/op 336 B/op 2 allocs/op +BenchmarkKocha_Param5 1000000 1682 ns/op 440 B/op 10 allocs/op +BenchmarkMacaron_Param5 300000 4596 ns/op 1376 B/op 14 allocs/op +BenchmarkMartini_Param5 100000 15703 ns/op 1280 B/op 12 allocs/op +BenchmarkPat_Param5 300000 5320 ns/op 1008 B/op 42 allocs/op +BenchmarkPossum_Param5 1000000 2155 ns/op 624 B/op 7 allocs/op +BenchmarkR2router_Param5 1000000 1559 ns/op 432 B/op 6 allocs/op +BenchmarkRevel_Param5 200000 8184 ns/op 2024 B/op 35 allocs/op +BenchmarkRivet_Param5 1000000 1914 ns/op 528 B/op 9 allocs/op +BenchmarkTango_Param5 1000000 3280 ns/op 944 B/op 18 allocs/op +BenchmarkTigerTonic_Param5 200000 11638 ns/op 2519 B/op 53 allocs/op +BenchmarkTraffic_Param5 200000 8941 ns/op 2280 B/op 31 allocs/op +BenchmarkVulcan_Param5 1000000 1279 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_Param5 1000000 1574 ns/op 416 B/op 3 allocs/op +BenchmarkAce_Param20 1000000 1528 ns/op 640 B/op 1 allocs/op +BenchmarkBear_Param20 300000 4906 ns/op 1633 B/op 5 allocs/op +BenchmarkBeego_Param20 200000 10529 ns/op 3868 B/op 17 allocs/op +BenchmarkBone_Param20 300000 7362 ns/op 2539 B/op 5 allocs/op +BenchmarkDenco_Param20 1000000 1884 ns/op 640 B/op 1 allocs/op +BenchmarkEcho_Param20 2000000 689 ns/op 0 B/op 0 allocs/op +BenchmarkGin_Param20 3000000 545 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_Param20 200000 9437 ns/op 3804 B/op 16 allocs/op +BenchmarkGoji_Param20 500000 3987 ns/op 1246 B/op 2 allocs/op +BenchmarkGoJsonRest_Param20 100000 12799 ns/op 4492 B/op 21 allocs/op +BenchmarkGoRestful_Param20 100000 19451 ns/op 5244 B/op 33 allocs/op +BenchmarkGorillaMux_Param20 100000 12456 ns/op 3275 B/op 11 allocs/op +BenchmarkHttpRouter_Param20 1000000 1333 ns/op 640 B/op 1 allocs/op +BenchmarkHttpTreeMux_Param20 300000 6490 ns/op 2187 B/op 4 allocs/op +BenchmarkKocha_Param20 300000 5335 ns/op 1808 B/op 27 allocs/op +BenchmarkMacaron_Param20 200000 11325 ns/op 4252 B/op 18 allocs/op +BenchmarkMartini_Param20 20000 64419 ns/op 3644 B/op 14 allocs/op +BenchmarkPat_Param20 50000 24672 ns/op 4888 B/op 151 allocs/op +BenchmarkPossum_Param20 1000000 2085 ns/op 624 B/op 7 allocs/op +BenchmarkR2router_Param20 300000 6809 ns/op 2283 B/op 8 allocs/op +BenchmarkRevel_Param20 100000 16600 ns/op 5551 B/op 54 allocs/op +BenchmarkRivet_Param20 200000 8428 ns/op 2620 B/op 26 allocs/op +BenchmarkTango_Param20 100000 16302 ns/op 8224 B/op 48 allocs/op +BenchmarkTigerTonic_Param20 30000 46828 ns/op 10538 B/op 178 allocs/op +BenchmarkTraffic_Param20 50000 28871 ns/op 7998 B/op 66 allocs/op +BenchmarkVulcan_Param20 1000000 2267 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_Param20 300000 6828 ns/op 2507 B/op 5 allocs/op +BenchmarkAce_ParamWrite 3000000 502 ns/op 40 B/op 2 allocs/op +BenchmarkBear_ParamWrite 1000000 1303 ns/op 424 B/op 5 allocs/op +BenchmarkBeego_ParamWrite 1000000 2489 ns/op 728 B/op 11 allocs/op +BenchmarkBone_ParamWrite 1000000 1181 ns/op 384 B/op 3 allocs/op +BenchmarkDenco_ParamWrite 5000000 315 ns/op 32 B/op 1 allocs/op +BenchmarkEcho_ParamWrite 10000000 237 ns/op 8 B/op 1 allocs/op +BenchmarkGin_ParamWrite 5000000 336 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_ParamWrite 1000000 2079 ns/op 664 B/op 10 allocs/op +BenchmarkGoji_ParamWrite 1000000 1092 ns/op 336 B/op 2 allocs/op +BenchmarkGoJsonRest_ParamWrite 1000000 3329 ns/op 1136 B/op 19 allocs/op +BenchmarkGoRestful_ParamWrite 200000 9273 ns/op 2504 B/op 32 allocs/op +BenchmarkGorillaMux_ParamWrite 500000 3919 ns/op 792 B/op 10 allocs/op +BenchmarkHttpRouter_ParamWrite 10000000 223 ns/op 32 B/op 1 allocs/op +BenchmarkHttpTreeMux_ParamWrite 2000000 788 ns/op 336 B/op 2 allocs/op +BenchmarkKocha_ParamWrite 3000000 549 ns/op 56 B/op 3 allocs/op +BenchmarkMacaron_ParamWrite 500000 4558 ns/op 1216 B/op 16 allocs/op +BenchmarkMartini_ParamWrite 200000 8850 ns/op 1256 B/op 16 allocs/op +BenchmarkPat_ParamWrite 500000 3679 ns/op 1088 B/op 19 allocs/op +BenchmarkPossum_ParamWrite 1000000 2114 ns/op 624 B/op 7 allocs/op +BenchmarkR2router_ParamWrite 1000000 1320 ns/op 432 B/op 6 allocs/op +BenchmarkRevel_ParamWrite 200000 8048 ns/op 2128 B/op 33 allocs/op +BenchmarkRivet_ParamWrite 1000000 1393 ns/op 472 B/op 6 allocs/op +BenchmarkTango_ParamWrite 2000000 819 ns/op 136 B/op 5 allocs/op +BenchmarkTigerTonic_ParamWrite 300000 5860 ns/op 1440 B/op 25 allocs/op +BenchmarkTraffic_ParamWrite 200000 7429 ns/op 2400 B/op 27 allocs/op +BenchmarkVulcan_ParamWrite 2000000 972 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_ParamWrite 1000000 1226 ns/op 368 B/op 3 allocs/op +BenchmarkAce_GithubStatic 5000000 294 ns/op 0 B/op 0 allocs/op +BenchmarkBear_GithubStatic 3000000 575 ns/op 88 B/op 3 allocs/op +BenchmarkBeego_GithubStatic 1000000 1561 ns/op 368 B/op 7 allocs/op +BenchmarkBone_GithubStatic 200000 12301 ns/op 2880 B/op 60 allocs/op +BenchmarkDenco_GithubStatic 20000000 74.6 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_GithubStatic 10000000 176 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GithubStatic 10000000 159 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GithubStatic 1000000 1116 ns/op 304 B/op 6 allocs/op +BenchmarkGoji_GithubStatic 5000000 413 ns/op 0 B/op 0 allocs/op +BenchmarkGoRestful_GithubStatic 30000 55200 ns/op 3520 B/op 36 allocs/op +BenchmarkGoJsonRest_GithubStatic 1000000 1504 ns/op 337 B/op 12 allocs/op +BenchmarkGorillaMux_GithubStatic 100000 23620 ns/op 464 B/op 8 allocs/op +BenchmarkHttpRouter_GithubStatic 20000000 78.3 ns/op 0 B/op 0 allocs/op +BenchmarkHttpTreeMux_GithubStatic 20000000 84.9 ns/op 0 B/op 0 allocs/op +BenchmarkKocha_GithubStatic 20000000 111 ns/op 0 B/op 0 allocs/op +BenchmarkMacaron_GithubStatic 1000000 2686 ns/op 752 B/op 8 allocs/op +BenchmarkMartini_GithubStatic 100000 22244 ns/op 832 B/op 11 allocs/op +BenchmarkPat_GithubStatic 100000 13278 ns/op 3648 B/op 76 allocs/op +BenchmarkPossum_GithubStatic 1000000 1429 ns/op 480 B/op 4 allocs/op +BenchmarkR2router_GithubStatic 2000000 726 ns/op 144 B/op 5 allocs/op +BenchmarkRevel_GithubStatic 300000 6271 ns/op 1288 B/op 25 allocs/op +BenchmarkRivet_GithubStatic 3000000 474 ns/op 112 B/op 2 allocs/op +BenchmarkTango_GithubStatic 1000000 1842 ns/op 256 B/op 10 allocs/op +BenchmarkTigerTonic_GithubStatic 5000000 361 ns/op 48 B/op 1 allocs/op +BenchmarkTraffic_GithubStatic 30000 47197 ns/op 18920 B/op 149 allocs/op +BenchmarkVulcan_GithubStatic 1000000 1415 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_GithubStatic 1000000 2522 ns/op 512 B/op 11 allocs/op +BenchmarkAce_GithubParam 3000000 578 ns/op 96 B/op 1 allocs/op +BenchmarkBear_GithubParam 1000000 1592 ns/op 464 B/op 5 allocs/op +BenchmarkBeego_GithubParam 1000000 2891 ns/op 784 B/op 11 allocs/op +BenchmarkBone_GithubParam 300000 6440 ns/op 1456 B/op 16 allocs/op +BenchmarkDenco_GithubParam 3000000 514 ns/op 128 B/op 1 allocs/op +BenchmarkEcho_GithubParam 5000000 292 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GithubParam 10000000 242 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GithubParam 1000000 2343 ns/op 720 B/op 10 allocs/op +BenchmarkGoji_GithubParam 1000000 1566 ns/op 336 B/op 2 allocs/op +BenchmarkGoJsonRest_GithubParam 1000000 2828 ns/op 721 B/op 15 allocs/op +BenchmarkGoRestful_GithubParam 10000 177711 ns/op 2816 B/op 35 allocs/op +BenchmarkGorillaMux_GithubParam 100000 13591 ns/op 816 B/op 9 allocs/op +BenchmarkHttpRouter_GithubParam 5000000 352 ns/op 96 B/op 1 allocs/op +BenchmarkHttpTreeMux_GithubParam 2000000 973 ns/op 336 B/op 2 allocs/op +BenchmarkKocha_GithubParam 2000000 889 ns/op 128 B/op 5 allocs/op +BenchmarkMacaron_GithubParam 500000 4047 ns/op 1168 B/op 12 allocs/op +BenchmarkMartini_GithubParam 50000 28982 ns/op 1184 B/op 12 allocs/op +BenchmarkPat_GithubParam 200000 8747 ns/op 2480 B/op 56 allocs/op +BenchmarkPossum_GithubParam 1000000 2158 ns/op 624 B/op 7 allocs/op +BenchmarkR2router_GithubParam 1000000 1352 ns/op 432 B/op 6 allocs/op +BenchmarkRevel_GithubParam 200000 7673 ns/op 1784 B/op 30 allocs/op +BenchmarkRivet_GithubParam 1000000 1573 ns/op 480 B/op 6 allocs/op +BenchmarkTango_GithubParam 1000000 2418 ns/op 480 B/op 13 allocs/op +BenchmarkTigerTonic_GithubParam 300000 6048 ns/op 1440 B/op 28 allocs/op +BenchmarkTraffic_GithubParam 100000 20143 ns/op 6024 B/op 55 allocs/op +BenchmarkVulcan_GithubParam 1000000 2224 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_GithubParam 500000 4156 ns/op 1312 B/op 12 allocs/op +BenchmarkAce_GithubAll 10000 109482 ns/op 13792 B/op 167 allocs/op +BenchmarkBear_GithubAll 10000 287490 ns/op 79952 B/op 943 allocs/op +BenchmarkBeego_GithubAll 3000 562184 ns/op 146272 B/op 2092 allocs/op +BenchmarkBone_GithubAll 500 2578716 ns/op 648016 B/op 8119 allocs/op +BenchmarkDenco_GithubAll 20000 94955 ns/op 20224 B/op 167 allocs/op +BenchmarkEcho_GithubAll 30000 58705 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GithubAll 30000 50991 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GithubAll 5000 449648 ns/op 133280 B/op 1889 allocs/op +BenchmarkGoji_GithubAll 2000 689748 ns/op 56113 B/op 334 allocs/op +BenchmarkGoJsonRest_GithubAll 5000 537769 ns/op 135995 B/op 2940 allocs/op +BenchmarkGoRestful_GithubAll 100 18410628 ns/op 797236 B/op 7725 allocs/op +BenchmarkGorillaMux_GithubAll 200 8036360 ns/op 153137 B/op 1791 allocs/op +BenchmarkHttpRouter_GithubAll 20000 63506 ns/op 13792 B/op 167 allocs/op +BenchmarkHttpTreeMux_GithubAll 10000 165927 ns/op 56112 B/op 334 allocs/op +BenchmarkKocha_GithubAll 10000 171362 ns/op 23304 B/op 843 allocs/op +BenchmarkMacaron_GithubAll 2000 817008 ns/op 224960 B/op 2315 allocs/op +BenchmarkMartini_GithubAll 100 12609209 ns/op 237952 B/op 2686 allocs/op +BenchmarkPat_GithubAll 300 4830398 ns/op 1504101 B/op 32222 allocs/op +BenchmarkPossum_GithubAll 10000 301716 ns/op 97440 B/op 812 allocs/op +BenchmarkR2router_GithubAll 10000 270691 ns/op 77328 B/op 1182 allocs/op +BenchmarkRevel_GithubAll 1000 1491919 ns/op 345553 B/op 5918 allocs/op +BenchmarkRivet_GithubAll 10000 283860 ns/op 84272 B/op 1079 allocs/op +BenchmarkTango_GithubAll 5000 473821 ns/op 87078 B/op 2470 allocs/op +BenchmarkTigerTonic_GithubAll 2000 1120131 ns/op 241088 B/op 6052 allocs/op +BenchmarkTraffic_GithubAll 200 8708979 ns/op 2664762 B/op 22390 allocs/op +BenchmarkVulcan_GithubAll 5000 353392 ns/op 19894 B/op 609 allocs/op +BenchmarkZeus_GithubAll 2000 944234 ns/op 300688 B/op 2648 allocs/op +BenchmarkAce_GPlusStatic 5000000 251 ns/op 0 B/op 0 allocs/op +BenchmarkBear_GPlusStatic 3000000 415 ns/op 72 B/op 3 allocs/op +BenchmarkBeego_GPlusStatic 1000000 1416 ns/op 352 B/op 7 allocs/op +BenchmarkBone_GPlusStatic 10000000 192 ns/op 32 B/op 1 allocs/op +BenchmarkDenco_GPlusStatic 30000000 47.6 ns/op 0 B/op 0 allocs/op +BenchmarkEcho_GPlusStatic 10000000 131 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GPlusStatic 10000000 131 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GPlusStatic 1000000 1035 ns/op 288 B/op 6 allocs/op +BenchmarkGoji_GPlusStatic 5000000 304 ns/op 0 B/op 0 allocs/op +BenchmarkGoJsonRest_GPlusStatic 1000000 1286 ns/op 337 B/op 12 allocs/op +BenchmarkGoRestful_GPlusStatic 200000 9649 ns/op 2160 B/op 30 allocs/op +BenchmarkGorillaMux_GPlusStatic 1000000 2346 ns/op 464 B/op 8 allocs/op +BenchmarkHttpRouter_GPlusStatic 30000000 42.7 ns/op 0 B/op 0 allocs/op +BenchmarkHttpTreeMux_GPlusStatic 30000000 49.5 ns/op 0 B/op 0 allocs/op +BenchmarkKocha_GPlusStatic 20000000 74.8 ns/op 0 B/op 0 allocs/op +BenchmarkMacaron_GPlusStatic 1000000 2520 ns/op 736 B/op 8 allocs/op +BenchmarkMartini_GPlusStatic 300000 5310 ns/op 832 B/op 11 allocs/op +BenchmarkPat_GPlusStatic 5000000 398 ns/op 96 B/op 2 allocs/op +BenchmarkPossum_GPlusStatic 1000000 1434 ns/op 480 B/op 4 allocs/op +BenchmarkR2router_GPlusStatic 2000000 646 ns/op 144 B/op 5 allocs/op +BenchmarkRevel_GPlusStatic 300000 6172 ns/op 1272 B/op 25 allocs/op +BenchmarkRivet_GPlusStatic 3000000 444 ns/op 112 B/op 2 allocs/op +BenchmarkTango_GPlusStatic 1000000 1400 ns/op 208 B/op 10 allocs/op +BenchmarkTigerTonic_GPlusStatic 10000000 213 ns/op 32 B/op 1 allocs/op +BenchmarkTraffic_GPlusStatic 1000000 3091 ns/op 1208 B/op 16 allocs/op +BenchmarkVulcan_GPlusStatic 2000000 863 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_GPlusStatic 10000000 237 ns/op 16 B/op 1 allocs/op +BenchmarkAce_GPlusParam 3000000 435 ns/op 64 B/op 1 allocs/op +BenchmarkBear_GPlusParam 1000000 1205 ns/op 448 B/op 5 allocs/op +BenchmarkBeego_GPlusParam 1000000 2494 ns/op 720 B/op 10 allocs/op +BenchmarkBone_GPlusParam 1000000 1126 ns/op 384 B/op 3 allocs/op +BenchmarkDenco_GPlusParam 5000000 325 ns/op 64 B/op 1 allocs/op +BenchmarkEcho_GPlusParam 10000000 168 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GPlusParam 10000000 170 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GPlusParam 1000000 1895 ns/op 656 B/op 9 allocs/op +BenchmarkGoji_GPlusParam 1000000 1071 ns/op 336 B/op 2 allocs/op +BenchmarkGoJsonRest_GPlusParam 1000000 2282 ns/op 657 B/op 14 allocs/op +BenchmarkGoRestful_GPlusParam 100000 19400 ns/op 2560 B/op 33 allocs/op +BenchmarkGorillaMux_GPlusParam 500000 5001 ns/op 784 B/op 9 allocs/op +BenchmarkHttpRouter_GPlusParam 10000000 240 ns/op 64 B/op 1 allocs/op +BenchmarkHttpTreeMux_GPlusParam 2000000 797 ns/op 336 B/op 2 allocs/op +BenchmarkKocha_GPlusParam 3000000 505 ns/op 56 B/op 3 allocs/op +BenchmarkMacaron_GPlusParam 1000000 3668 ns/op 1104 B/op 11 allocs/op +BenchmarkMartini_GPlusParam 200000 10672 ns/op 1152 B/op 12 allocs/op +BenchmarkPat_GPlusParam 1000000 2376 ns/op 704 B/op 14 allocs/op +BenchmarkPossum_GPlusParam 1000000 2090 ns/op 624 B/op 7 allocs/op +BenchmarkR2router_GPlusParam 1000000 1233 ns/op 432 B/op 6 allocs/op +BenchmarkRevel_GPlusParam 200000 6778 ns/op 1704 B/op 28 allocs/op +BenchmarkRivet_GPlusParam 1000000 1279 ns/op 464 B/op 5 allocs/op +BenchmarkTango_GPlusParam 1000000 1981 ns/op 272 B/op 10 allocs/op +BenchmarkTigerTonic_GPlusParam 500000 3893 ns/op 1064 B/op 19 allocs/op +BenchmarkTraffic_GPlusParam 200000 6585 ns/op 2000 B/op 23 allocs/op +BenchmarkVulcan_GPlusParam 1000000 1233 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_GPlusParam 1000000 1350 ns/op 368 B/op 3 allocs/op +BenchmarkAce_GPlus2Params 3000000 512 ns/op 64 B/op 1 allocs/op +BenchmarkBear_GPlus2Params 1000000 1564 ns/op 464 B/op 5 allocs/op +BenchmarkBeego_GPlus2Params 1000000 3043 ns/op 784 B/op 11 allocs/op +BenchmarkBone_GPlus2Params 1000000 3152 ns/op 736 B/op 7 allocs/op +BenchmarkDenco_GPlus2Params 3000000 431 ns/op 64 B/op 1 allocs/op +BenchmarkEcho_GPlus2Params 5000000 247 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GPlus2Params 10000000 219 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GPlus2Params 1000000 2363 ns/op 720 B/op 10 allocs/op +BenchmarkGoji_GPlus2Params 1000000 1540 ns/op 336 B/op 2 allocs/op +BenchmarkGoJsonRest_GPlus2Params 1000000 2872 ns/op 721 B/op 15 allocs/op +BenchmarkGoRestful_GPlus2Params 100000 23030 ns/op 2720 B/op 35 allocs/op +BenchmarkGorillaMux_GPlus2Params 200000 10516 ns/op 816 B/op 9 allocs/op +BenchmarkHttpRouter_GPlus2Params 5000000 273 ns/op 64 B/op 1 allocs/op +BenchmarkHttpTreeMux_GPlus2Params 2000000 939 ns/op 336 B/op 2 allocs/op +BenchmarkKocha_GPlus2Params 2000000 844 ns/op 128 B/op 5 allocs/op +BenchmarkMacaron_GPlus2Params 500000 3914 ns/op 1168 B/op 12 allocs/op +BenchmarkMartini_GPlus2Params 50000 35759 ns/op 1280 B/op 16 allocs/op +BenchmarkPat_GPlus2Params 200000 7089 ns/op 2304 B/op 41 allocs/op +BenchmarkPossum_GPlus2Params 1000000 2093 ns/op 624 B/op 7 allocs/op +BenchmarkR2router_GPlus2Params 1000000 1320 ns/op 432 B/op 6 allocs/op +BenchmarkRevel_GPlus2Params 200000 7351 ns/op 1800 B/op 30 allocs/op +BenchmarkRivet_GPlus2Params 1000000 1485 ns/op 480 B/op 6 allocs/op +BenchmarkTango_GPlus2Params 1000000 2111 ns/op 448 B/op 12 allocs/op +BenchmarkTigerTonic_GPlus2Params 300000 6271 ns/op 1528 B/op 28 allocs/op +BenchmarkTraffic_GPlus2Params 100000 14886 ns/op 3312 B/op 34 allocs/op +BenchmarkVulcan_GPlus2Params 1000000 1883 ns/op 98 B/op 3 allocs/op +BenchmarkZeus_GPlus2Params 1000000 2686 ns/op 784 B/op 6 allocs/op +BenchmarkAce_GPlusAll 300000 5912 ns/op 640 B/op 11 allocs/op +BenchmarkBear_GPlusAll 100000 16448 ns/op 5072 B/op 61 allocs/op +BenchmarkBeego_GPlusAll 50000 32916 ns/op 8976 B/op 129 allocs/op +BenchmarkBone_GPlusAll 50000 25836 ns/op 6992 B/op 76 allocs/op +BenchmarkDenco_GPlusAll 500000 4462 ns/op 672 B/op 11 allocs/op +BenchmarkEcho_GPlusAll 500000 2806 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GPlusAll 500000 2579 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GPlusAll 50000 25223 ns/op 8144 B/op 116 allocs/op +BenchmarkGoji_GPlusAll 100000 14237 ns/op 3696 B/op 22 allocs/op +BenchmarkGoJsonRest_GPlusAll 50000 29227 ns/op 8221 B/op 183 allocs/op +BenchmarkGoRestful_GPlusAll 10000 203144 ns/op 36064 B/op 441 allocs/op +BenchmarkGorillaMux_GPlusAll 20000 80906 ns/op 9712 B/op 115 allocs/op +BenchmarkHttpRouter_GPlusAll 500000 3040 ns/op 640 B/op 11 allocs/op +BenchmarkHttpTreeMux_GPlusAll 200000 9627 ns/op 3696 B/op 22 allocs/op +BenchmarkKocha_GPlusAll 200000 8108 ns/op 976 B/op 43 allocs/op +BenchmarkMacaron_GPlusAll 30000 48083 ns/op 13968 B/op 142 allocs/op +BenchmarkMartini_GPlusAll 10000 196978 ns/op 15072 B/op 178 allocs/op +BenchmarkPat_GPlusAll 30000 58865 ns/op 16880 B/op 343 allocs/op +BenchmarkPossum_GPlusAll 100000 19685 ns/op 6240 B/op 52 allocs/op +BenchmarkR2router_GPlusAll 100000 16251 ns/op 5040 B/op 76 allocs/op +BenchmarkRevel_GPlusAll 20000 93489 ns/op 21656 B/op 368 allocs/op +BenchmarkRivet_GPlusAll 100000 16907 ns/op 5408 B/op 64 allocs/op +``` \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index c89a3c23..938084c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,90 @@ -#Changelog +#CHANGELOG -###Gin 0.5 (Aug 21, 2014) +###Gin 1.0rc2 (...) + +- [PERFORMANCE] Fast path for writting Content-Type. +- [PERFORMANCE] Much faster 404 routing +- [PERFORMANCE] Allocation optimizations +- [PERFORMANCE] Faster root tree lookup +- [PERFORMANCE] Zero overhead, String() and JSON() rendering. +- [PERFORMANCE] Faster ClientIP parsing +- [PERFORMANCE] Much faster SSE implementation +- [NEW] Benchmarks suite +- [NEW] Bind validation can be disabled and replaced with custom validators. +- [NEW] More flexible HTML render +- [FIX] Binding multipart form +- [FIX] Integration tests +- [FIX] Crash when binding non struct object in Context. +- [FIX] RunTLS() implementation +- [FIX] Logger() unit tests +- [FIX] Better approach to avoid directory listing in StaticFS() +- [FIX] Context.ClientIP() always returns the IP with trimmed spaces. +- [FIX] Better warning when running in debug mode. +- [FIX] Google App Engine integration. debugPrint does not use os.Stdout +- [FIX] Fixes integer overflow in error type +- [FIX] Error implements the json.Marshaller interface +- [FIX] MIT license in every file + + +###Gin 1.0rc1 (May 22, 2015) + +- [PERFORMANCE] Zero allocation router +- [PERFORMANCE] Faster JSON, XML and text rendering +- [PERFORMANCE] Custom hand optimized HttpRouter for Gin +- [PERFORMANCE] Misc code optimizations. Inlining, tail call optimizations +- [NEW] Built-in support for golang.org/x/net/context +- [NEW] Any(path, handler). Create a route that matches any path +- [NEW] Refactored rendering pipeline (faster and static typeded) +- [NEW] Refactored errors API +- [NEW] IndentedJSON() prints pretty JSON +- [NEW] Added gin.DefaultWriter +- [NEW] UNIX socket support +- [NEW] RouterGroup.BasePath is exposed +- [NEW] JSON validation using go-validate-yourself (very powerful options) +- [NEW] Completed suite of unit tests +- [NEW] HTTP streaming with c.Stream() +- [NEW] StaticFile() creates a router for serving just one file. +- [NEW] StaticFS() has an option to disable directory listing. +- [NEW] StaticFS() for serving static files through virtual filesystems +- [NEW] Server-Sent Events native support +- [NEW] WrapF() and WrapH() helpers for wrapping http.HandlerFunc and http.Handler +- [NEW] Added LoggerWithWriter() middleware +- [NEW] Added RecoveryWithWriter() middleware +- [NEW] Added DefaultPostFormValue() +- [NEW] Added DefaultFormValue() +- [NEW] Added DefaultParamValue() +- [FIX] BasicAuth() when using custom realm +- [FIX] Bug when serving static files in nested routing group +- [FIX] Redirect using built-in http.Redirect() +- [FIX] Logger when printing the requested path +- [FIX] Documentation typos +- [FIX] Context.Engine renamed to Context.engine +- [FIX] Better debugging messages +- [FIX] ErrorLogger +- [FIX] Debug HTTP render +- [FIX] Refactored binding and render modules +- [FIX] Refactored Context initialization +- [FIX] Refactored BasicAuth() +- [FIX] NoMethod/NoRoute handlers +- [FIX] Hijacking http +- [FIX] Better support for Google App Engine (using log instead of fmt) + + +###Gin 0.6 (Mar 9, 2015) + +- [NEW] Support multipart/form-data +- [NEW] NoMethod handler +- [NEW] Validate sub structures +- [NEW] Support for HTTP Realm Auth +- [FIX] Unsigned integers in binding +- [FIX] Improve color logger + + +###Gin 0.5 (Feb 7, 2015) - [NEW] Content Negotiation +- [FIX] Solved security bug that allow a client to spoof ip +- [FIX] Fix unexported/ignored fields in binding ###Gin 0.4 (Aug 21, 2014) @@ -39,7 +121,7 @@ - [NEW] New API for serving static files. gin.Static() - [NEW] gin.H() can be serialized into XML - [NEW] Typed errors. Errors can be typed. Internet/external/custom. -- [NEW] Support for Godebs +- [NEW] Support for Godeps - [NEW] Travis/Godocs badges in README - [NEW] New Bind() and BindWith() methods for parsing request body. - [NEW] Add Content.Copy() diff --git a/Godeps/Godeps.json b/Godeps/Godeps.json index d963b7ea..ebda5138 100644 --- a/Godeps/Godeps.json +++ b/Godeps/Godeps.json @@ -1,10 +1,39 @@ { "ImportPath": "github.com/gin-gonic/gin", - "GoVersion": "go1.3", + "GoVersion": "go1.4.2", + "Packages": [ + "./..." + ], "Deps": [ { - "ImportPath": "github.com/julienschmidt/httprouter", - "Rev": "7deadb6844d2c6ff1dfb812eaa439b87cdaedf20" + "ImportPath": "github.com/dustin/go-broadcast", + "Rev": "3bdf6d4a7164a50bc19d5f230e2981d87d2584f1" + }, + { + "ImportPath": "github.com/manucorporat/sse", + "Rev": "c142f0f1baea5cef7f98a8a6c222f6134368c1f5" + }, + { + "ImportPath": "github.com/manucorporat/stats", + "Rev": "8f2d6ace262eba462e9beb552382c98be51d807b" + }, + { + "ImportPath": "github.com/mattn/go-colorable", + "Rev": "d67e0b7d1797975196499f79bcc322c08b9f218b" + }, + { + "ImportPath": "github.com/stretchr/testify/assert", + "Comment": "v1.0", + "Rev": "232e8563676cd15c3a36ba5e675ad4312ac4cb11" + }, + { + "ImportPath": "golang.org/x/net/context", + "Rev": "621fff363a1d9ad7fdd0bfa9d80a42881267deb4" + }, + { + "ImportPath": "gopkg.in/bluesuncorp/validator.v5", + "Comment": "v5.4", + "Rev": "07cbdd2e6dfd947b002e83c13b775c7580fab2d5" } ] } diff --git a/README.md b/README.md index 82a1584d..8d2e07fb 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,10 @@ -#Gin Web Framework +#Gin Web Framework [![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) [![Coverage Status](https://coveralls.io/repos/gin-gonic/gin/badge.svg?branch=master)](https://coveralls.io/r/gin-gonic/gin?branch=master) -[![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) -[![Build Status](https://travis-ci.org/gin-gonic/gin.svg)](https://travis-ci.org/gin-gonic/gin) + [![GoDoc](https://godoc.org/github.com/gin-gonic/gin?status.svg)](https://godoc.org/github.com/gin-gonic/gin) [![Join the chat at https://gitter.im/gin-gonic/gin](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/gin-gonic/gin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) -Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster. If you need performance and good productivity, you will love Gin. +Gin is a web framework written in Golang. It features a martini-like API with much better performance, up to 40 times faster thanks to [httprouter](https://github.com/julienschmidt/httprouter). If you need performance and good productivity, you will love Gin. -![Gin console logger](http://forzefield.com/gin_example.png) +![Gin console logger](https://gin-gonic.github.io/gin/other/console.png) ``` $ cat test.go @@ -15,108 +14,94 @@ package main import "github.com/gin-gonic/gin" -func main() { - router := gin.Default() - router.GET("/", func(c *gin.Context) { - c.String(200, "hello world") - }) - router.GET("/ping", func(c *gin.Context) { - c.String(200, "pong") - }) - router.POST("/submit", func(c *gin.Context) { - c.String(401, "not authorized") - }) - router.PUT("/error", func(c *gin.Context) { - c.String(500, "and error hapenned :(") - }) - router.Run(":8080") -} -``` - -##Gin is new, will it be supported? - -Yes, Gin is an internal project of [my](https://github.com/manucorporat) upcoming startup. We developed it and we are going to continue using and improve it. - - -##Roadmap for v1.0 -- [x] Performance improments, reduce allocation and garbage collection overhead -- [x] Fix bugs -- [ ] Stable API -- [ ] Ask our designer for a cool logo -- [ ] Add tons of unit tests -- [ ] Add internal benchmarks suite -- [x] Improve logging system -- [x] Improve JSON/XML validation using bindings -- [x] Improve XML support -- [x] Flexible rendering system -- [ ] More powerful validation API -- [ ] Improve documentation -- [ ] Add more cool middlewares, for example redis caching (this also helps developers to understand the framework). -- [x] Continuous integration - - - -## Start using it -Obviously, you need to have Git and Go already installed to run Gin. -Run this in your terminal - -``` -go get github.com/gin-gonic/gin -``` -Then import it in your Go code: - -``` -import "github.com/gin-gonic/gin" -``` - - -##Community -If you'd like to help out with the project, there's a mailing list and IRC channel where Gin discussions normally happen. - -* IRC - * [irc.freenode.net #getgin](irc://irc.freenode.net:6667/getgin) - * [Webchat](http://webchat.freenode.net?randomnick=1&channels=%23getgin) -* Mailing List - * Subscribe: [getgin@librelist.org](mailto:getgin@librelist.org) - * [Archives](http://librelist.com/browser/getgin/) - - -##API Examples - -#### Create most basic PING/PONG HTTP endpoint -```go -package main - -import "github.com/gin-gonic/gin" - func main() { r := gin.Default() r.GET("/ping", func(c *gin.Context) { c.String(200, "pong") }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + r.Run(":8080") // listen and serve on 0.0.0.0:8080 } ``` +## Benchmarks + +Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) + +[See all benchmarks](/BENCHMARKS.md) + + +``` +BenchmarkAce_GithubAll 10000 109482 ns/op 13792 B/op 167 allocs/op +BenchmarkBear_GithubAll 10000 287490 ns/op 79952 B/op 943 allocs/op +BenchmarkBeego_GithubAll 3000 562184 ns/op 146272 B/op 2092 allocs/op +BenchmarkBone_GithubAll 500 2578716 ns/op 648016 B/op 8119 allocs/op +BenchmarkDenco_GithubAll 20000 94955 ns/op 20224 B/op 167 allocs/op +BenchmarkEcho_GithubAll 30000 58705 ns/op 0 B/op 0 allocs/op +BenchmarkGin_GithubAll 30000 50991 ns/op 0 B/op 0 allocs/op +BenchmarkGocraftWeb_GithubAll 5000 449648 ns/op 133280 B/op 1889 allocs/op +BenchmarkGoji_GithubAll 2000 689748 ns/op 56113 B/op 334 allocs/op +BenchmarkGoJsonRest_GithubAll 5000 537769 ns/op 135995 B/op 2940 allocs/op +BenchmarkGoRestful_GithubAll 100 18410628 ns/op 797236 B/op 7725 allocs/op +BenchmarkGorillaMux_GithubAll 200 8036360 ns/op 153137 B/op 1791 allocs/op +BenchmarkHttpRouter_GithubAll 20000 63506 ns/op 13792 B/op 167 allocs/op +BenchmarkHttpTreeMux_GithubAll 10000 165927 ns/op 56112 B/op 334 allocs/op +BenchmarkKocha_GithubAll 10000 171362 ns/op 23304 B/op 843 allocs/op +BenchmarkMacaron_GithubAll 2000 817008 ns/op 224960 B/op 2315 allocs/op +BenchmarkMartini_GithubAll 100 12609209 ns/op 237952 B/op 2686 allocs/op +BenchmarkPat_GithubAll 300 4830398 ns/op 1504101 B/op 32222 allocs/op +BenchmarkPossum_GithubAll 10000 301716 ns/op 97440 B/op 812 allocs/op +BenchmarkR2router_GithubAll 10000 270691 ns/op 77328 B/op 1182 allocs/op +BenchmarkRevel_GithubAll 1000 1491919 ns/op 345553 B/op 5918 allocs/op +BenchmarkRivet_GithubAll 10000 283860 ns/op 84272 B/op 1079 allocs/op +BenchmarkTango_GithubAll 5000 473821 ns/op 87078 B/op 2470 allocs/op +BenchmarkTigerTonic_GithubAll 2000 1120131 ns/op 241088 B/op 6052 allocs/op +BenchmarkTraffic_GithubAll 200 8708979 ns/op 2664762 B/op 22390 allocs/op +BenchmarkVulcan_GithubAll 5000 353392 ns/op 19894 B/op 609 allocs/op +BenchmarkZeus_GithubAll 2000 944234 ns/op 300688 B/op 2648 allocs/op +``` + + +##Gin v1. stable + +- [x] Zero allocation router. +- [x] Still the fastest http router and framework. From routing to writing. +- [x] Complete suite of unit tests +- [x] Battle tested +- [x] API frozen, new releases will not break your code. + + +## Start using it +1. Download and install it: + +```sh +go get github.com/gin-gonic/gin +``` +2. Import it in your code: + +```go +import "github.com/gin-gonic/gin" +``` + +##API Examples + #### Using GET, POST, PUT, PATCH, DELETE and OPTIONS ```go func main() { - // Creates a gin router + logger and recovery (crash-free) middlewares - r := gin.Default() + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() - r.GET("/someGet", getting) - r.POST("/somePost", posting) - r.PUT("/somePut", putting) - r.DELETE("/someDelete", deleting) - r.PATCH("/somePatch", patching) - r.HEAD("/someHead", head) - r.OPTIONS("/someOptions", options) + router.GET("/someGet", getting) + router.POST("/somePost", posting) + router.PUT("/somePut", putting) + router.DELETE("/someDelete", deleting) + router.PATCH("/somePatch", patching) + router.HEAD("/someHead", head) + router.OPTIONS("/someOptions", options) // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + router.Run(":8080") } ``` @@ -124,36 +109,70 @@ func main() { ```go func main() { - r := gin.Default() - + router := gin.Default() + // This handler will match /user/john but will not match neither /user/ or /user - r.GET("/user/:name", func(c *gin.Context) { - name := c.Params.ByName("name") - message := "Hello "+name - c.String(200, message) + router.GET("/user/:name", func(c *gin.Context) { + name := c.Param("name") + c.String(http.StatusOK, "Hello %s", name) }) - // However, this one will match /user/john and also /user/john/send - r.GET("/user/:name/*action", func(c *gin.Context) { - name := c.Params.ByName("name") - action := c.Params.ByName("action") + // However, this one will match /user/john/ and also /user/john/send + // If no other routers match /user/john, it will redirect to /user/join/ + router.GET("/user/:name/*action", func(c *gin.Context) { + name := c.Param("name") + action := c.Param("action") message := name + " is " + action - c.String(200, message) + c.String(http.StatusOK, message) }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + + router.Run(":8080") } ``` +#### Querystring parameters +```go +func main() { + router := gin.Default() + + // Query string parameters are parsed using the existing underlying request object. + // The request responds to a url matching: /welcome?firstname=Jane&lastname=Doe + router.GET("/welcome", func(c *gin.Context) { + firstname := c.DefaultQuery("firstname", "Guest") + lastname := c.Query("lastname") // shortcut for c.Request.URL.Query().Get("lastname") + + c.String(http.StatusOK, "Hello %s %s", firstname, lastname) + }) + router.Run(":8080") +} +``` + +### Multipart/Urlencoded Form + +```go +func main() { + router := gin.Default() + + router.POST("/form_post", func(c *gin.Context) { + message := c.PostForm("message") + nick := c.DefaultPostForm("nick", "anonymous") + + c.JSON(200, gin.H{ + "status": "posted", + "message": message, + }) + }) + router.Run(":8080") +} +``` #### Grouping routes ```go func main() { - r := gin.Default() + router := gin.Default() // Simple group: v1 - v1 := r.Group("/v1") + v1 := router.Group("/v1") { v1.POST("/login", loginEndpoint) v1.POST("/submit", submitEndpoint) @@ -161,20 +180,19 @@ func main() { } // Simple group: v2 - v2 := r.Group("/v2") + v2 := router.Group("/v2") { v2.POST("/login", loginEndpoint) v2.POST("/submit", submitEndpoint) v2.POST("/read", readEndpoint) } - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + router.Run(":8080") } ``` -#### Blank Gin without middlewares by default +#### Blank Gin without middleware by default Use @@ -188,24 +206,24 @@ r := gin.Default() ``` -#### Using middlewares +#### Using middleware ```go func main() { // Creates a router without any middleware by default r := gin.New() - // Global middlewares + // Global middleware r.Use(gin.Logger()) r.Use(gin.Recovery()) - // Per route middlewares, you can add as many as you desire. + // Per route middleware, you can add as many as you desire. r.GET("/benchmark", MyBenchLogger(), benchEndpoint) // Authorization group // authorized := r.Group("/", AuthRequired()) // exactly the same than: authorized := r.Group("/") - // per group middlewares! in this case we use the custom created + // per group middleware! in this case we use the custom created // AuthRequired() middleware just in the "authorized" group. authorized.Use(AuthRequired()) { @@ -229,7 +247,7 @@ To bind a request body into a type, use model binding. We currently support bind Note that you need to set the corresponding binding tag on all fields you want to bind. For example, when binding from JSON, set `json:"fieldname"`. -When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use BindWith. +When using the Bind-method, Gin tries to infer the binder depending on the Content-Type header. If you are sure what you are binding, you can use BindWith. You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, the current request will fail with an error. @@ -243,33 +261,33 @@ type LoginJSON struct { // Binding from form values type LoginForm struct { User string `form:"user" binding:"required"` - Password string `form:"password" binding:"required"` + Password string `form:"password" binding:"required"` } func main() { r := gin.Default() // Example for binding JSON ({"user": "manu", "password": "123"}) - r.POST("/login", func(c *gin.Context) { + r.POST("/loginJSON", func(c *gin.Context) { var json LoginJSON c.Bind(&json) // This will infer what binder to use depending on the content-type header. if json.User == "manu" && json.Password == "123" { - c.JSON(200, gin.H{"status": "you are logged in"}) + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else { - c.JSON(401, gin.H{"status": "unauthorized"}) + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) } }) // Example for binding a HTML form (user=manu&password=123) - r.POST("/login", func(c *gin.Context) { + r.POST("/loginHTML", func(c *gin.Context) { var form LoginForm c.BindWith(&form, binding.Form) // You can also specify which binder to use. We support binding.Form, binding.JSON and binding.XML. if form.User == "manu" && form.Password == "123" { - c.JSON(200, gin.H{"status": "you are logged in"}) + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) } else { - c.JSON(401, gin.H{"status": "unauthorized"}) + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) } }) @@ -278,15 +296,59 @@ func main() { } ``` + +###Multipart/Urlencoded binding +```go +package main + +import ( + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" +) + +type LoginForm struct { + User string `form:"user" binding:"required"` + Password string `form:"password" binding:"required"` +} + +func main() { + + router := gin.Default() + + router.POST("/login", func(c *gin.Context) { + // you can bind multipart form with explicit binding declaration: + // c.BindWith(&form, binding.Form) + // or you can simply use autobinding with Bind method: + var form LoginForm + c.Bind(&form) // in this case proper binding will be automatically selected + + if form.User == "user" && form.Password == "password" { + c.JSON(200, gin.H{"status": "you are logged in"}) + } else { + c.JSON(401, gin.H{"status": "unauthorized"}) + } + }) + + router.Run(":8080") + +} +``` + +Test it with: +```bash +$ curl -v --form user=user --form password=password http://localhost:8080/login +``` + + #### XML and JSON rendering ```go func main() { r := gin.Default() - // gin.H is a shortcup for map[string]interface{} + // gin.H is a shortcut for map[string]interface{} r.GET("/someJSON", func(c *gin.Context) { - c.JSON(200, gin.H{"message": "hey", "status": 200}) + c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) }) r.GET("/moreJSON", func(c *gin.Context) { @@ -301,11 +363,11 @@ func main() { msg.Number = 123 // Note that msg.Name becomes "user" in the JSON // Will output : {"user": "Lena", "Message": "hey", "Number": 123} - c.JSON(200, msg) + c.JSON(http.StatusOK, msg) }) r.GET("/someXML", func(c *gin.Context) { - c.XML(200, gin.H{"message": "hey", "status": 200}) + c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) }) // Listen and server on 0.0.0.0:8080 @@ -313,6 +375,19 @@ func main() { } ``` +####Serving static files + +```go +func main() { + router := gin.Default() + router.Static("/assets", "./assets") + router.StaticFS("/more_static", http.Dir("my_file_system")) + router.StaticFile("/favicon.ico", "./resources/favicon.ico") + + // Listen and server on 0.0.0.0:8080 + router.Run(":8080") +} +``` ####HTML rendering @@ -320,17 +395,23 @@ Using LoadHTMLTemplates() ```go func main() { - r := gin.Default() - r.LoadHTMLTemplates("templates/*") - r.GET("/index", func(c *gin.Context) { - obj := gin.H{"title": "Main website"} - c.HTML(200, "index.tmpl", obj) + router := gin.Default() + router.LoadHTMLGlob("templates/*") + //router.LoadHTMLFiles("templates/template1.html", "templates/template2.html") + router.GET("/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "index.tmpl", gin.H{ + "title": "Main website", + }) }) - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + router.Run(":8080") } ``` +```html +

+ {{ .title }} +

+ +``` You can also use your own html template render @@ -338,28 +419,27 @@ You can also use your own html template render import "html/template" func main() { - r := gin.Default() + router := gin.Default() html := template.Must(template.ParseFiles("file1", "file2")) - r.HTMLTemplates = html - - // Listen and server on 0.0.0.0:8080 - r.Run(":8080") + router.SetHTMLTemplate(html) + router.Run(":8080") } ``` + #### Redirects Issuing a HTTP redirect is easy: ```go r.GET("/test", func(c *gin.Context) { - c.Redirect(301, "http://www.google.com/") + c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") }) ``` Both internal and external locations are supported. -#### Custom Middlewares +#### Custom Middleware ```go func Logger() gin.HandlerFunc { @@ -401,7 +481,7 @@ func main() { #### Using BasicAuth() middleware ```go -// similate some private data +// simulate some private data var secrets = gin.H{ "foo": gin.H{"email": "foo@bar.com", "phone": "123433"}, "austin": gin.H{"email": "austin@example.com", "phone": "666"}, @@ -424,11 +504,11 @@ func main() { // hit "localhost:8080/admin/secrets authorized.GET("/secrets", func(c *gin.Context) { // get user, it was setted by the BasicAuth middleware - user := c.Get(gin.AuthUserKey).(string) + user := c.MustGet(gin.AuthUserKey).(string) if secret, ok := secrets[user]; ok { - c.JSON(200, gin.H{"user": user, "secret": secret}) + c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) } else { - c.JSON(200, gin.H{"user": user, "secret": "NO SECRET :("}) + c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) } }) diff --git a/auth.go b/auth.go index 7602d726..33f8e9a3 100644 --- a/auth.go +++ b/auth.go @@ -7,8 +7,7 @@ package gin import ( "crypto/subtle" "encoding/base64" - "errors" - "sort" + "strconv" ) const ( @@ -24,64 +23,69 @@ type ( authPairs []authPair ) -func (a authPairs) Len() int { return len(a) } -func (a authPairs) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a authPairs) Less(i, j int) bool { return a[i].Value < a[j].Value } - -// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where -// the key is the user name and the value is the password. -func BasicAuth(accounts Accounts) HandlerFunc { - pairs, err := processAccounts(accounts) - if err != nil { - panic(err) +func (a authPairs) searchCredential(authValue string) (string, bool) { + if len(authValue) == 0 { + return "", false } + for _, pair := range a { + if pair.Value == authValue { + return pair.User, true + } + } + return "", false +} + +// Implements a basic Basic HTTP Authorization. It takes as arguments a map[string]string where +// the key is the user name and the value is the password, as well as the name of the Realm +// (see http://tools.ietf.org/html/rfc2617#section-1.2) +func BasicAuthForRealm(accounts Accounts, realm string) HandlerFunc { + if realm == "" { + realm = "Authorization Required" + } + realm = "Basic realm=" + strconv.Quote(realm) + pairs := processAccounts(accounts) return func(c *Context) { // Search user in the slice of allowed credentials - user, ok := searchCredential(pairs, c.Request.Header.Get("Authorization")) - if !ok { - // Credentials doesn't match, we return 401 Unauthorized and abort request. - c.Writer.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"") - c.Fail(401, errors.New("Unauthorized")) + user, found := pairs.searchCredential(c.Request.Header.Get("Authorization")) + if !found { + // Credentials doesn't match, we return 401 and abort handlers chain. + c.Header("WWW-Authenticate", realm) + c.AbortWithStatus(401) } else { - // user is allowed, set UserId to key "user" in this context, the userId can be read later using - // c.Get(gin.AuthUserKey) + // The user credentials was found, set user's id to key AuthUserKey in this context, the userId can be read later using + // c.MustGet(gin.AuthUserKey) c.Set(AuthUserKey, user) } } } -func processAccounts(accounts Accounts) (authPairs, error) { +// Implements a basic Basic HTTP Authorization. It takes as argument a map[string]string where +// the key is the user name and the value is the password. +func BasicAuth(accounts Accounts) HandlerFunc { + return BasicAuthForRealm(accounts, "") +} + +func processAccounts(accounts Accounts) authPairs { if len(accounts) == 0 { - return nil, errors.New("Empty list of authorized credentials") + panic("Empty list of authorized credentials") } pairs := make(authPairs, 0, len(accounts)) for user, password := range accounts { if len(user) == 0 { - return nil, errors.New("User can not be empty") + panic("User can not be empty") } - base := user + ":" + password - value := "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) + value := authorizationHeader(user, password) pairs = append(pairs, authPair{ Value: value, User: user, }) } - // We have to sort the credentials in order to use bsearch later. - sort.Sort(pairs) - return pairs, nil + return pairs } -func searchCredential(pairs authPairs, auth string) (string, bool) { - if len(auth) == 0 { - return "", false - } - // Search user in the slice of allowed credentials - r := sort.Search(len(pairs), func(i int) bool { return pairs[i].Value >= auth }) - if r < len(pairs) && secureCompare(pairs[r].Value, auth) { - return pairs[r].User, true - } else { - return "", false - } +func authorizationHeader(user, password string) string { + base := user + ":" + password + return "Basic " + base64.StdEncoding.EncodeToString([]byte(base)) } func secureCompare(given, actual string) bool { diff --git a/auth_test.go b/auth_test.go index d60c587b..b22d9ced 100644 --- a/auth_test.go +++ b/auth_test.go @@ -9,53 +9,138 @@ import ( "net/http" "net/http/httptest" "testing" + + "github.com/stretchr/testify/assert" ) -func TestBasicAuthSucceed(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() - - r := New() - accounts := Accounts{"admin": "password"} - r.Use(BasicAuth(accounts)) - - r.GET("/login", func(c *Context) { - c.String(200, "autorized") +func TestBasicAuth(t *testing.T) { + pairs := processAccounts(Accounts{ + "admin": "password", + "foo": "bar", + "bar": "foo", }) - req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + assert.Len(t, pairs, 3) + assert.Contains(t, pairs, authPair{ + User: "bar", + Value: "Basic YmFyOmZvbw==", + }) + assert.Contains(t, pairs, authPair{ + User: "foo", + Value: "Basic Zm9vOmJhcg==", + }) + assert.Contains(t, pairs, authPair{ + User: "admin", + Value: "Basic YWRtaW46cGFzc3dvcmQ=", + }) +} - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) - } - bodyAsString := w.Body.String() +func TestBasicAuthFails(t *testing.T) { + assert.Panics(t, func() { processAccounts(nil) }) + assert.Panics(t, func() { + processAccounts(Accounts{ + "": "password", + "foo": "bar", + }) + }) +} - if bodyAsString != "autorized" { - t.Errorf("Response body should be `autorized`, was %s", bodyAsString) - } +func TestBasicAuthSearchCredential(t *testing.T) { + pairs := processAccounts(Accounts{ + "admin": "password", + "foo": "bar", + "bar": "foo", + }) + + user, found := pairs.searchCredential(authorizationHeader("admin", "password")) + assert.Equal(t, user, "admin") + assert.True(t, found) + + user, found = pairs.searchCredential(authorizationHeader("foo", "bar")) + assert.Equal(t, user, "foo") + assert.True(t, found) + + user, found = pairs.searchCredential(authorizationHeader("bar", "foo")) + assert.Equal(t, user, "bar") + assert.True(t, found) + + user, found = pairs.searchCredential(authorizationHeader("admins", "password")) + assert.Empty(t, user) + assert.False(t, found) + + user, found = pairs.searchCredential(authorizationHeader("foo", "bar ")) + assert.Empty(t, user) + assert.False(t, found) + + user, found = pairs.searchCredential("") + assert.Empty(t, user) + assert.False(t, found) +} + +func TestBasicAuthAuthorizationHeader(t *testing.T) { + assert.Equal(t, authorizationHeader("admin", "password"), "Basic YWRtaW46cGFzc3dvcmQ=") +} + +func TestBasicAuthSecureCompare(t *testing.T) { + assert.True(t, secureCompare("1234567890", "1234567890")) + assert.False(t, secureCompare("123456789", "1234567890")) + assert.False(t, secureCompare("12345678900", "1234567890")) + assert.False(t, secureCompare("1234567891", "1234567890")) +} + +func TestBasicAuthSucceed(t *testing.T) { + accounts := Accounts{"admin": "password"} + router := New() + router.Use(BasicAuth(accounts)) + router.GET("/login", func(c *Context) { + c.String(200, c.MustGet(AuthUserKey).(string)) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) + req.Header.Set("Authorization", authorizationHeader("admin", "password")) + router.ServeHTTP(w, req) + + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "admin") } func TestBasicAuth401(t *testing.T) { - req, _ := http.NewRequest("GET", "/login", nil) - w := httptest.NewRecorder() - - r := New() + called := false accounts := Accounts{"foo": "bar"} - r.Use(BasicAuth(accounts)) - - r.GET("/login", func(c *Context) { - c.String(200, "autorized") + router := New() + router.Use(BasicAuth(accounts)) + router.GET("/login", func(c *Context) { + called = true + c.String(200, c.MustGet(AuthUserKey).(string)) }) + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) - r.ServeHTTP(w, req) + router.ServeHTTP(w, req) - if w.Code != 401 { - t.Errorf("Response code should be Not autorized, was: %s", w.Code) - } - - if w.HeaderMap.Get("WWW-Authenticate") != "Basic realm=\"Authorization Required\"" { - t.Errorf("WWW-Authenticate header is incorrect: %s", w.HeaderMap.Get("Content-Type")) - } + assert.False(t, called) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"Authorization Required\"") +} + +func TestBasicAuth401WithCustomRealm(t *testing.T) { + called := false + accounts := Accounts{"foo": "bar"} + router := New() + router.Use(BasicAuthForRealm(accounts, "My Custom \"Realm\"")) + router.GET("/login", func(c *Context) { + called = true + c.String(200, c.MustGet(AuthUserKey).(string)) + }) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/login", nil) + req.Header.Set("Authorization", "Basic "+base64.StdEncoding.EncodeToString([]byte("admin:password"))) + router.ServeHTTP(w, req) + + assert.False(t, called) + assert.Equal(t, w.Code, 401) + assert.Equal(t, w.HeaderMap.Get("WWW-Authenticate"), "Basic realm=\"My Custom \\\"Realm\\\"\"") } diff --git a/benchmarks_test.go b/benchmarks_test.go new file mode 100644 index 00000000..8a1c91a9 --- /dev/null +++ b/benchmarks_test.go @@ -0,0 +1,157 @@ +package gin + +import ( + "html/template" + "net/http" + "testing" +) + +func BenchmarkOneRoute(B *testing.B) { + router := New() + router.GET("/ping", func(c *Context) {}) + runRequest(B, router, "GET", "/ping") +} + +func BenchmarkRecoveryMiddleware(B *testing.B) { + router := New() + router.Use(Recovery()) + router.GET("/", func(c *Context) {}) + runRequest(B, router, "GET", "/") +} + +func BenchmarkLoggerMiddleware(B *testing.B) { + router := New() + router.Use(LoggerWithWriter(newMockWriter())) + router.GET("/", func(c *Context) {}) + runRequest(B, router, "GET", "/") +} + +func BenchmarkManyHandlers(B *testing.B) { + router := New() + router.Use(Recovery(), LoggerWithWriter(newMockWriter())) + router.Use(func(c *Context) {}) + router.Use(func(c *Context) {}) + router.GET("/ping", func(c *Context) {}) + runRequest(B, router, "GET", "/ping") +} + +func Benchmark5Params(B *testing.B) { + DefaultWriter = newMockWriter() + router := New() + router.Use(func(c *Context) {}) + router.GET("/param/:param1/:params2/:param3/:param4/:param5", func(c *Context) {}) + runRequest(B, router, "GET", "/param/path/to/parameter/john/12345") +} + +func BenchmarkOneRouteJSON(B *testing.B) { + router := New() + data := struct { + Status string `json:"status"` + }{"ok"} + router.GET("/json", func(c *Context) { + c.JSON(200, data) + }) + runRequest(B, router, "GET", "/json") +} + +var htmlContentType = []string{"text/html; charset=utf-8"} + +func BenchmarkOneRouteHTML(B *testing.B) { + router := New() + t := template.Must(template.New("index").Parse(` +

{{.}}

`)) + router.SetHTMLTemplate(t) + + router.GET("/html", func(c *Context) { + c.HTML(200, "index", "hola") + }) + runRequest(B, router, "GET", "/html") +} + +func BenchmarkOneRouteSet(B *testing.B) { + router := New() + router.GET("/ping", func(c *Context) { + c.Set("key", "value") + }) + runRequest(B, router, "GET", "/ping") +} + +func BenchmarkOneRouteString(B *testing.B) { + router := New() + router.GET("/text", func(c *Context) { + c.String(200, "this is a plain text") + }) + runRequest(B, router, "GET", "/text") +} + +func BenchmarkManyRoutesFist(B *testing.B) { + router := New() + router.Any("/ping", func(c *Context) {}) + runRequest(B, router, "GET", "/ping") +} + +func BenchmarkManyRoutesLast(B *testing.B) { + router := New() + router.Any("/ping", func(c *Context) {}) + runRequest(B, router, "OPTIONS", "/ping") +} + +func Benchmark404(B *testing.B) { + router := New() + router.Any("/something", func(c *Context) {}) + router.NoRoute(func(c *Context) {}) + runRequest(B, router, "GET", "/ping") +} + +func Benchmark404Many(B *testing.B) { + router := New() + router.GET("/", func(c *Context) {}) + router.GET("/path/to/something", func(c *Context) {}) + router.GET("/post/:id", func(c *Context) {}) + router.GET("/view/:id", func(c *Context) {}) + router.GET("/favicon.ico", func(c *Context) {}) + router.GET("/robots.txt", func(c *Context) {}) + router.GET("/delete/:id", func(c *Context) {}) + router.GET("/user/:id/:mode", func(c *Context) {}) + + router.NoRoute(func(c *Context) {}) + runRequest(B, router, "GET", "/viewfake") +} + +type mockWriter struct { + headers http.Header +} + +func newMockWriter() *mockWriter { + return &mockWriter{ + http.Header{}, + } +} + +func (m *mockWriter) Header() (h http.Header) { + return m.headers +} + +func (m *mockWriter) Write(p []byte) (n int, err error) { + return len(p), nil +} + +func (m *mockWriter) WriteString(s string) (n int, err error) { + return len(s), nil +} + +func (m *mockWriter) WriteHeader(int) {} + +func runRequest(B *testing.B, r *Engine, method, path string) { + // create fake request + req, err := http.NewRequest(method, path, nil) + if err != nil { + panic(err) + } + w := newMockWriter() + B.ReportAllocs() + B.ResetTimer() + for i := 0; i < B.N; i++ { + r.ServeHTTP(w, req) + } +} diff --git a/binding/binding.go b/binding/binding.go index 72b23dfc..f719fbc1 100644 --- a/binding/binding.go +++ b/binding/binding.go @@ -4,206 +4,58 @@ package binding -import ( - "encoding/json" - "encoding/xml" - "errors" - "net/http" - "reflect" - "strconv" - "strings" +import "net/http" + +const ( + MIMEJSON = "application/json" + MIMEHTML = "text/html" + MIMEXML = "application/xml" + MIMEXML2 = "text/xml" + MIMEPlain = "text/plain" + MIMEPOSTForm = "application/x-www-form-urlencoded" + MIMEMultipartPOSTForm = "multipart/form-data" ) -type ( - Binding interface { - Bind(*http.Request, interface{}) error - } +type Binding interface { + Name() string + Bind(*http.Request, interface{}) error +} - // JSON binding - jsonBinding struct{} +type StructValidator interface { + // ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right. + // If the received type is not a struct, any validation should be skipped and nil must be returned. + // If the received type is a struct or pointer to a struct, the validation should be performed. + // If the struct is not valid or the validation itself fails, a descriptive error should be returned. + // Otherwise nil must be returned. + ValidateStruct(interface{}) error +} - // XML binding - xmlBinding struct{} - - // // form binding - formBinding struct{} -) +var Validator StructValidator = &defaultValidator{} var ( JSON = jsonBinding{} XML = xmlBinding{} - Form = formBinding{} // todo + Form = formBinding{} ) -func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { - decoder := json.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) +func Default(method, contentType string) Binding { + if method == "GET" { + return Form } else { - return err + switch contentType { + case MIMEJSON: + return JSON + case MIMEXML, MIMEXML2: + return XML + default: //case MIMEPOSTForm, MIMEMultipartPOSTForm: + return Form + } } } -func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { - decoder := xml.NewDecoder(req.Body) - if err := decoder.Decode(obj); err == nil { - return Validate(obj) - } else { - return err - } -} - -func (_ formBinding) Bind(req *http.Request, obj interface{}) error { - if err := req.ParseForm(); err != nil { - return err - } - if err := mapForm(obj, req.Form); err != nil { - return err - } - return Validate(obj) -} - -func mapForm(ptr interface{}, form map[string][]string) error { - typ := reflect.TypeOf(ptr).Elem() - formStruct := reflect.ValueOf(ptr).Elem() - for i := 0; i < typ.NumField(); i++ { - typeField := typ.Field(i) - if inputFieldName := typeField.Tag.Get("form"); inputFieldName != "" { - structField := formStruct.Field(i) - if !structField.CanSet() { - continue - } - - inputValue, exists := form[inputFieldName] - if !exists { - continue - } - numElems := len(inputValue) - if structField.Kind() == reflect.Slice && numElems > 0 { - sliceOf := structField.Type().Elem().Kind() - slice := reflect.MakeSlice(structField.Type(), numElems, numElems) - for i := 0; i < numElems; i++ { - if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { - return err - } - } - formStruct.Field(i).Set(slice) - } else { - if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { - return err - } - } - } - } - return nil -} - -func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { - switch valueKind { - case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: - if val == "" { - val = "0" - } - intVal, err := strconv.Atoi(val) - if err != nil { - return err - } else { - structField.SetInt(int64(intVal)) - } - case reflect.Bool: - if val == "" { - val = "false" - } - boolVal, err := strconv.ParseBool(val) - if err != nil { - return err - } else { - structField.SetBool(boolVal) - } - case reflect.Float32: - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, 32) - if err != nil { - return err - } else { - structField.SetFloat(floatVal) - } - case reflect.Float64: - if val == "" { - val = "0.0" - } - floatVal, err := strconv.ParseFloat(val, 64) - if err != nil { - return err - } else { - structField.SetFloat(floatVal) - } - case reflect.String: - structField.SetString(val) - } - return nil -} - -// Don't pass in pointers to bind to. Can lead to bugs. See: -// https://github.com/codegangsta/martini-contrib/issues/40 -// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 -func ensureNotPointer(obj interface{}) { - if reflect.TypeOf(obj).Kind() == reflect.Ptr { - panic("Pointers are not accepted as binding models") - } -} - -func Validate(obj interface{}) error { - typ := reflect.TypeOf(obj) - val := reflect.ValueOf(obj) - - if typ.Kind() == reflect.Ptr { - typ = typ.Elem() - val = val.Elem() - } - - switch typ.Kind() { - case reflect.Struct: - for i := 0; i < typ.NumField(); i++ { - field := typ.Field(i) - - // Allow ignored and unexported fields in the struct - if field.Tag.Get("form") == "-" || field.PkgPath != "" { - continue - } - - fieldValue := val.Field(i).Interface() - zero := reflect.Zero(field.Type).Interface() - - if strings.Index(field.Tag.Get("binding"), "required") > -1 { - fieldType := field.Type.Kind() - if fieldType == reflect.Struct { - err := Validate(fieldValue) - if err != nil { - return err - } - } else if reflect.DeepEqual(zero, fieldValue) { - return errors.New("Required " + field.Name) - } else if fieldType == reflect.Slice && field.Type.Elem().Kind() == reflect.Struct { - err := Validate(fieldValue) - if err != nil { - return err - } - } - } - } - case reflect.Slice: - for i := 0; i < val.Len(); i++ { - fieldValue := val.Index(i).Interface() - err := Validate(fieldValue) - if err != nil { - return err - } - } - default: +func validate(obj interface{}) error { + if Validator == nil { return nil } - return nil + return Validator.ValidateStruct(obj) } diff --git a/binding/binding_test.go b/binding/binding_test.go new file mode 100644 index 00000000..db1678e4 --- /dev/null +++ b/binding/binding_test.go @@ -0,0 +1,123 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "bytes" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +type FooStruct struct { + Foo string `json:"foo" form:"foo" xml:"foo" binding:"required"` +} + +type FooBarStruct struct { + FooStruct + Bar string `json:"bar" form:"bar" xml:"bar" binding:"required"` +} + +func TestBindingDefault(t *testing.T) { + assert.Equal(t, Default("GET", ""), Form) + assert.Equal(t, Default("GET", MIMEJSON), Form) + + assert.Equal(t, Default("POST", MIMEJSON), JSON) + assert.Equal(t, Default("PUT", MIMEJSON), JSON) + + assert.Equal(t, Default("POST", MIMEXML), XML) + assert.Equal(t, Default("PUT", MIMEXML2), XML) + + assert.Equal(t, Default("POST", MIMEPOSTForm), Form) + assert.Equal(t, Default("PUT", MIMEPOSTForm), Form) + + assert.Equal(t, Default("POST", MIMEMultipartPOSTForm), Form) + assert.Equal(t, Default("PUT", MIMEMultipartPOSTForm), Form) +} + +func TestBindingJSON(t *testing.T) { + testBodyBinding(t, + JSON, "json", + "/", "/", + `{"foo": "bar"}`, `{"bar": "foo"}`) +} + +func TestBindingForm(t *testing.T) { + testFormBinding(t, "POST", + "/", "/", + "foo=bar&bar=foo", "bar2=foo") +} + +func TestBindingForm2(t *testing.T) { + testFormBinding(t, "GET", + "/?foo=bar&bar=foo", "/?bar2=foo", + "", "") +} + +func TestBindingXML(t *testing.T) { + testBodyBinding(t, + XML, "xml", + "/", "/", + "bar", "foo") +} + +func TestValidationFails(t *testing.T) { + var obj FooStruct + req := requestWithBody("POST", "/", `{"bar": "foo"}`) + err := JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func TestValidationDisabled(t *testing.T) { + backup := Validator + Validator = nil + defer func() { Validator = backup }() + + var obj FooStruct + req := requestWithBody("POST", "/", `{"bar": "foo"}`) + err := JSON.Bind(req, &obj) + assert.NoError(t, err) +} + +func testFormBinding(t *testing.T, method, path, badPath, body, badBody string) { + b := Form + assert.Equal(t, b.Name(), "form") + + obj := FooBarStruct{} + req := requestWithBody(method, path, body) + if method == "POST" { + req.Header.Add("Content-Type", MIMEPOSTForm) + } + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, obj.Bar, "foo") + + obj = FooBarStruct{} + req = requestWithBody(method, badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func testBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) { + assert.Equal(t, b.Name(), name) + + obj := FooStruct{} + req := requestWithBody("POST", path, body) + err := b.Bind(req, &obj) + assert.NoError(t, err) + assert.Equal(t, obj.Foo, "bar") + + obj = FooStruct{} + req = requestWithBody("POST", badPath, badBody) + err = JSON.Bind(req, &obj) + assert.Error(t, err) +} + +func requestWithBody(method, path, body string) (req *http.Request) { + req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) + return +} diff --git a/binding/default_validator.go b/binding/default_validator.go new file mode 100644 index 00000000..7f12152b --- /dev/null +++ b/binding/default_validator.go @@ -0,0 +1,40 @@ +package binding + +import ( + "reflect" + "sync" + + "gopkg.in/bluesuncorp/validator.v5" +) + +type defaultValidator struct { + once sync.Once + validate *validator.Validate +} + +var _ StructValidator = &defaultValidator{} + +func (v *defaultValidator) ValidateStruct(obj interface{}) error { + if kindOfData(obj) == reflect.Struct { + v.lazyinit() + if err := v.validate.Struct(obj); err != nil { + return error(err) + } + } + return nil +} + +func (v *defaultValidator) lazyinit() { + v.once.Do(func() { + v.validate = validator.New("binding", validator.BakedInValidators) + }) +} + +func kindOfData(data interface{}) reflect.Kind { + value := reflect.ValueOf(data) + valueType := value.Kind() + if valueType == reflect.Ptr { + valueType = value.Elem().Kind() + } + return valueType +} diff --git a/binding/form.go b/binding/form.go new file mode 100644 index 00000000..ff00b0df --- /dev/null +++ b/binding/form.go @@ -0,0 +1,24 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import "net/http" + +type formBinding struct{} + +func (_ formBinding) Name() string { + return "form" +} + +func (_ formBinding) Bind(req *http.Request, obj interface{}) error { + if err := req.ParseForm(); err != nil { + return err + } + req.ParseMultipartForm(32 << 10) // 32 MB + if err := mapForm(obj, req.Form); err != nil { + return err + } + return validate(obj) +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go new file mode 100644 index 00000000..d8b13b1e --- /dev/null +++ b/binding/form_mapping.go @@ -0,0 +1,151 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "errors" + "reflect" + "strconv" +) + +func mapForm(ptr interface{}, form map[string][]string) error { + typ := reflect.TypeOf(ptr).Elem() + val := reflect.ValueOf(ptr).Elem() + for i := 0; i < typ.NumField(); i++ { + typeField := typ.Field(i) + structField := val.Field(i) + if !structField.CanSet() { + continue + } + + structFieldKind := structField.Kind() + inputFieldName := typeField.Tag.Get("form") + if inputFieldName == "" { + inputFieldName = typeField.Name + + // if "form" tag is nil, we inspect if the field is a struct. + // this would not make sense for JSON parsing but it does for a form + // since data is flatten + if structFieldKind == reflect.Struct { + err := mapForm(structField.Addr().Interface(), form) + if err != nil { + return err + } + continue + } + } + inputValue, exists := form[inputFieldName] + if !exists { + continue + } + + numElems := len(inputValue) + if structFieldKind == reflect.Slice && numElems > 0 { + sliceOf := structField.Type().Elem().Kind() + slice := reflect.MakeSlice(structField.Type(), numElems, numElems) + for i := 0; i < numElems; i++ { + if err := setWithProperType(sliceOf, inputValue[i], slice.Index(i)); err != nil { + return err + } + } + val.Field(i).Set(slice) + } else { + if err := setWithProperType(typeField.Type.Kind(), inputValue[0], structField); err != nil { + return err + } + } + + } + return nil +} + +func setWithProperType(valueKind reflect.Kind, val string, structField reflect.Value) error { + switch valueKind { + case reflect.Int: + return setIntField(val, 0, structField) + case reflect.Int8: + return setIntField(val, 8, structField) + case reflect.Int16: + return setIntField(val, 16, structField) + case reflect.Int32: + return setIntField(val, 32, structField) + case reflect.Int64: + return setIntField(val, 64, structField) + case reflect.Uint: + return setUintField(val, 0, structField) + case reflect.Uint8: + return setUintField(val, 8, structField) + case reflect.Uint16: + return setUintField(val, 16, structField) + case reflect.Uint32: + return setUintField(val, 32, structField) + case reflect.Uint64: + return setUintField(val, 64, structField) + case reflect.Bool: + return setBoolField(val, structField) + case reflect.Float32: + return setFloatField(val, 32, structField) + case reflect.Float64: + return setFloatField(val, 64, structField) + case reflect.String: + structField.SetString(val) + default: + return errors.New("Unknown type") + } + return nil +} + +func setIntField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + intVal, err := strconv.ParseInt(val, 10, bitSize) + if err == nil { + field.SetInt(intVal) + } + return err +} + +func setUintField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0" + } + uintVal, err := strconv.ParseUint(val, 10, bitSize) + if err == nil { + field.SetUint(uintVal) + } + return err +} + +func setBoolField(val string, field reflect.Value) error { + if val == "" { + val = "false" + } + boolVal, err := strconv.ParseBool(val) + if err == nil { + field.SetBool(boolVal) + } + return nil +} + +func setFloatField(val string, bitSize int, field reflect.Value) error { + if val == "" { + val = "0.0" + } + floatVal, err := strconv.ParseFloat(val, bitSize) + if err == nil { + field.SetFloat(floatVal) + } + return err +} + +// Don't pass in pointers to bind to. Can lead to bugs. See: +// https://github.com/codegangsta/martini-contrib/issues/40 +// https://github.com/codegangsta/martini-contrib/pull/34#issuecomment-29683659 +func ensureNotPointer(obj interface{}) { + if reflect.TypeOf(obj).Kind() == reflect.Ptr { + panic("Pointers are not accepted as binding models") + } +} diff --git a/binding/json.go b/binding/json.go new file mode 100644 index 00000000..25c5a06c --- /dev/null +++ b/binding/json.go @@ -0,0 +1,25 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "encoding/json" + + "net/http" +) + +type jsonBinding struct{} + +func (_ jsonBinding) Name() string { + return "json" +} + +func (_ jsonBinding) Bind(req *http.Request, obj interface{}) error { + decoder := json.NewDecoder(req.Body) + if err := decoder.Decode(obj); err != nil { + return err + } + return validate(obj) +} diff --git a/binding/validate_test.go b/binding/validate_test.go new file mode 100644 index 00000000..27ba7b66 --- /dev/null +++ b/binding/validate_test.go @@ -0,0 +1,69 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +type struct1 struct { + Value float64 `binding:"required"` +} + +type struct2 struct { + RequiredValue string `binding:"required"` + Value float64 +} + +type struct3 struct { + Integer int + String string + BasicSlice []int + Boolean bool + + RequiredInteger int `binding:"required"` + RequiredString string `binding:"required"` + RequiredAnotherStruct struct1 `binding:"required"` + RequiredBasicSlice []int `binding:"required"` + RequiredComplexSlice []struct2 `binding:"required"` + RequiredBoolean bool `binding:"required"` +} + +func createStruct() struct3 { + return struct3{ + RequiredInteger: 2, + RequiredString: "hello", + RequiredAnotherStruct: struct1{1.5}, + RequiredBasicSlice: []int{1, 2, 3, 4}, + RequiredComplexSlice: []struct2{ + {RequiredValue: "A"}, + {RequiredValue: "B"}, + }, + RequiredBoolean: true, + } +} + +func TestValidateGoodObject(t *testing.T) { + test := createStruct() + assert.Nil(t, validate(&test)) +} + +type Object map[string]interface{} +type MyObjects []Object + +func TestValidateSlice(t *testing.T) { + var obj MyObjects + var obj2 Object + var nu = 10 + + assert.NoError(t, validate(obj)) + assert.NoError(t, validate(&obj)) + assert.NoError(t, validate(obj2)) + assert.NoError(t, validate(&obj2)) + assert.NoError(t, validate(nu)) + assert.NoError(t, validate(&nu)) +} diff --git a/binding/xml.go b/binding/xml.go new file mode 100644 index 00000000..cac4be89 --- /dev/null +++ b/binding/xml.go @@ -0,0 +1,24 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package binding + +import ( + "encoding/xml" + "net/http" +) + +type xmlBinding struct{} + +func (_ xmlBinding) Name() string { + return "xml" +} + +func (_ xmlBinding) Bind(req *http.Request, obj interface{}) error { + decoder := xml.NewDecoder(req.Body) + if err := decoder.Decode(obj); err != nil { + return err + } + return validate(obj) +} diff --git a/context.go b/context.go index 82251249..126130f3 100644 --- a/context.go +++ b/context.go @@ -5,55 +5,30 @@ package gin import ( - "bytes" "errors" - "fmt" + "io" + "math" + "net/http" + "strings" + "time" + "github.com/gin-gonic/gin/binding" "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" - "log" - "net/http" + "github.com/manucorporat/sse" + "golang.org/x/net/context" ) const ( - ErrorTypeInternal = 1 << iota - ErrorTypeExternal = 1 << iota - ErrorTypeAll = 0xffffffff + MIMEJSON = binding.MIMEJSON + MIMEHTML = binding.MIMEHTML + MIMEXML = binding.MIMEXML + MIMEXML2 = binding.MIMEXML2 + MIMEPlain = binding.MIMEPlain + MIMEPOSTForm = binding.MIMEPOSTForm + MIMEMultipartPOSTForm = binding.MIMEMultipartPOSTForm ) -// Used internally to collect errors that occurred during an http request. -type errorMsg struct { - Err string `json:"error"` - Type uint32 `json:"-"` - Meta interface{} `json:"meta"` -} - -type errorMsgs []errorMsg - -func (a errorMsgs) ByType(typ uint32) errorMsgs { - if len(a) == 0 { - return a - } - result := make(errorMsgs, 0, len(a)) - for _, msg := range a { - if msg.Type&typ > 0 { - result = append(result, msg) - } - } - return result -} - -func (a errorMsgs) String() string { - if len(a) == 0 { - return "" - } - var buffer bytes.Buffer - for i, msg := range a { - text := fmt.Sprintf("Error #%02d: %s \n Meta: %v\n", (i + 1), msg.Err, msg.Meta) - buffer.WriteString(text) - } - return buffer.String() -} +const AbortIndex int8 = math.MaxInt8 / 2 // Context is the most important part of gin. It allows us to pass variables between middleware, // manage the flow, validate the JSON of a request and render a JSON response for example. @@ -61,48 +36,47 @@ type Context struct { writermem responseWriter Request *http.Request Writer ResponseWriter - Keys map[string]interface{} - Errors errorMsgs - Params httprouter.Params - Engine *Engine - handlers []HandlerFunc - index int8 - accepted []string + + Params Params + handlers HandlersChain + index int8 + + engine *Engine + Keys map[string]interface{} + Errors errorMsgs + Accepted []string } +var _ context.Context = &Context{} + /************************************/ /********** CONTEXT CREATION ********/ /************************************/ -func (engine *Engine) createContext(w http.ResponseWriter, req *http.Request, params httprouter.Params, handlers []HandlerFunc) *Context { - c := engine.pool.Get().(*Context) - c.writermem.reset(w) - c.Request = req - c.Params = params - c.handlers = handlers - c.Keys = nil +func (c *Context) reset() { + c.Writer = &c.writermem + c.Params = c.Params[0:0] + c.handlers = nil c.index = -1 - c.accepted = nil + c.Keys = nil c.Errors = c.Errors[0:0] - return c -} - -func (engine *Engine) reuseContext(c *Context) { - engine.pool.Put(c) + c.Accepted = nil } func (c *Context) Copy() *Context { var cp Context = *c + cp.writermem.ResponseWriter = nil + cp.Writer = &cp.writermem cp.index = AbortIndex cp.handlers = nil return &cp } /************************************/ -/*************** FLOW ***************/ +/*********** FLOW CONTROL ***********/ /************************************/ -// Next should be used only in the middlewares. +// Next should be used only inside middleware. // It executes the pending handlers in the chain inside the calling handler. // See example in github. func (c *Context) Next() { @@ -113,201 +87,315 @@ func (c *Context) Next() { } } -// Forces the system to do not continue calling the pending handlers in the chain. +// Returns if the currect context was aborted. +func (c *Context) IsAborted() bool { + return c.index == AbortIndex +} + +// Stops the system to continue calling the pending handlers in the chain. +// Let's say you have an authorization middleware that validates if the request is authorized +// if the authorization fails (the password does not match). This method (Abort()) should be called +// in order to stop the execution of the actual handler. func (c *Context) Abort() { c.index = AbortIndex } -// Same than AbortWithStatus() but also writes the specified response status code. -// For example, the first handler checks if the request is authorized. If it's not, context.AbortWithStatus(401) should be called. +// It calls Abort() and writes the headers with the specified status code. +// For example, a failed attempt to authentificate a request could use: context.AbortWithStatus(401). func (c *Context) AbortWithStatus(code int) { c.Writer.WriteHeader(code) c.Abort() } +// It calls AbortWithStatus() and Error() internally. This method stops the chain, writes the status code and +// pushes the specified error to `c.Errors`. +// See Context.Error() for more details. +func (c *Context) AbortWithError(code int, err error) *Error { + c.AbortWithStatus(code) + return c.Error(err) +} + /************************************/ /********* ERROR MANAGEMENT *********/ /************************************/ -// Fail is the same as Abort plus an error message. -// Calling `context.Fail(500, err)` is equivalent to: -// ``` -// context.Error("Operation aborted", err) -// context.AbortWithStatus(500) -// ``` -func (c *Context) Fail(code int, err error) { - c.Error(err, "Operation aborted") - c.AbortWithStatus(code) -} - -func (c *Context) ErrorTyped(err error, typ uint32, meta interface{}) { - c.Errors = append(c.Errors, errorMsg{ - Err: err.Error(), - Type: typ, - Meta: meta, - }) -} - // Attaches an error to the current context. The error is pushed to a list of errors. // It's a good idea to call Error for each error that occurred during the resolution of a request. // A middleware can be used to collect all the errors and push them to a database together, print a log, or append it in the HTTP response. -func (c *Context) Error(err error, meta interface{}) { - c.ErrorTyped(err, ErrorTypeExternal, meta) -} - -func (c *Context) LastError() error { - nuErrors := len(c.Errors) - if nuErrors > 0 { - return errors.New(c.Errors[nuErrors-1].Err) - } else { - return nil +func (c *Context) Error(err error) *Error { + var parsedError *Error + switch err.(type) { + case *Error: + parsedError = err.(*Error) + default: + parsedError = &Error{ + Err: err, + Type: ErrorTypePrivate, + } } + c.Errors = append(c.Errors, parsedError) + return parsedError } /************************************/ /******** METADATA MANAGEMENT********/ /************************************/ -// Sets a new pair key/value just for the specified context. -// It also lazy initializes the hashmap. -func (c *Context) Set(key string, item interface{}) { +// Sets a new pair key/value just for this context. +// It also lazy initializes the hashmap if it was not used previously. +func (c *Context) Set(key string, value interface{}) { if c.Keys == nil { c.Keys = make(map[string]interface{}) } - c.Keys[key] = item + c.Keys[key] = value } -// Get returns the value for the given key or an error if the key does not exist. -func (c *Context) Get(key string) (interface{}, error) { +// Returns the value for the given key, ie: (value, true). +// If the value does not exists it returns (nil, false) +func (c *Context) Get(key string) (value interface{}, exists bool) { if c.Keys != nil { - value, ok := c.Keys[key] - if ok { - return value, nil + value, exists = c.Keys[key] + } + return +} + +// Returns the value for the given key if it exists, otherwise it panics. +func (c *Context) MustGet(key string) interface{} { + if value, exists := c.Get(key); exists { + return value + } + panic("Key \"" + key + "\" does not exist") +} + +/************************************/ +/************ INPUT DATA ************/ +/************************************/ + +// Shortcut for c.Request.URL.Query().Get(key) +func (c *Context) Query(key string) (va string) { + va, _ = c.query(key) + return +} + +// Shortcut for c.Request.PostFormValue(key) +func (c *Context) PostForm(key string) (va string) { + va, _ = c.postForm(key) + return +} + +// Shortcut for c.Params.ByName(key) +func (c *Context) Param(key string) string { + return c.Params.ByName(key) +} + +func (c *Context) DefaultPostForm(key, defaultValue string) string { + if va, ok := c.postForm(key); ok { + return va + } + return defaultValue +} + +func (c *Context) DefaultQuery(key, defaultValue string) string { + if va, ok := c.query(key); ok { + return va + } + return defaultValue +} + +func (c *Context) query(key string) (string, bool) { + req := c.Request + if values, ok := req.URL.Query()[key]; ok && len(values) > 0 { + return values[0], true + } + return "", false +} + +func (c *Context) postForm(key string) (string, bool) { + req := c.Request + req.ParseMultipartForm(32 << 20) // 32 MB + if values := req.PostForm[key]; len(values) > 0 { + return values[0], true + } + if req.MultipartForm != nil && req.MultipartForm.File != nil { + if values := req.MultipartForm.Value[key]; len(values) > 0 { + return values[0], true } } - return nil, errors.New("Key does not exist.") + return "", false } -// MustGet returns the value for the given key or panics if the value doesn't exist. -func (c *Context) MustGet(key string) interface{} { - value, err := c.Get(key) - if err != nil || value == nil { - log.Panicf("Key %s doesn't exist", value) - } - return value -} - -func (c *Context) ClientIP() string { - clientIP := c.Request.Header.Get("X-Real-IP") - if len(clientIP) == 0 { - clientIP = c.Request.Header.Get("X-Forwarded-For") - } - if len(clientIP) == 0 { - clientIP = c.Request.RemoteAddr - } - return clientIP -} - -/************************************/ -/********* PARSING REQUEST **********/ -/************************************/ - // This function checks the Content-Type to select a binding engine automatically, // Depending the "Content-Type" header different bindings are used: // "application/json" --> JSON binding // "application/xml" --> XML binding // else --> returns an error // if Parses the request's body as JSON if Content-Type == "application/json" using JSON or XML as a JSON input. It decodes the json payload into the struct specified as a pointer.Like ParseBody() but this method also writes a 400 error if the json is not valid. -func (c *Context) Bind(obj interface{}) bool { - var b binding.Binding - ctype := filterFlags(c.Request.Header.Get("Content-Type")) - switch { - case c.Request.Method == "GET" || ctype == MIMEPOSTForm: - b = binding.Form - case ctype == MIMEJSON: - b = binding.JSON - case ctype == MIMEXML || ctype == MIMEXML2: - b = binding.XML - default: - c.Fail(400, errors.New("unknown content-type: "+ctype)) - return false - } +func (c *Context) Bind(obj interface{}) error { + b := binding.Default(c.Request.Method, c.ContentType()) return c.BindWith(obj, b) } -func (c *Context) BindWith(obj interface{}, b binding.Binding) bool { +// Shortcut for c.BindWith(obj, binding.JSON) +func (c *Context) BindJSON(obj interface{}) error { + return c.BindWith(obj, binding.JSON) +} + +func (c *Context) BindWith(obj interface{}, b binding.Binding) error { if err := b.Bind(c.Request, obj); err != nil { - c.Fail(400, err) - return false + c.AbortWithError(400, err).SetType(ErrorTypeBind) + return err } - return true + return nil +} + +// Best effort algoritm to return the real client IP, it parses +// X-Real-IP and X-Forwarded-For in order to work properly with reverse-proxies such us: nginx or haproxy. +func (c *Context) ClientIP() string { + if c.engine.ForwardedByClientIP { + clientIP := strings.TrimSpace(c.requestHeader("X-Real-Ip")) + if len(clientIP) > 0 { + return clientIP + } + clientIP = c.requestHeader("X-Forwarded-For") + if index := strings.IndexByte(clientIP, ','); index >= 0 { + clientIP = clientIP[0:index] + } + clientIP = strings.TrimSpace(clientIP) + if len(clientIP) > 0 { + return clientIP + } + } + return strings.TrimSpace(c.Request.RemoteAddr) +} + +func (c *Context) ContentType() string { + return filterFlags(c.requestHeader("Content-Type")) +} + +func (c *Context) requestHeader(key string) string { + if values, _ := c.Request.Header[key]; len(values) > 0 { + return values[0] + } + return "" } /************************************/ /******** RESPONSE RENDERING ********/ /************************************/ -func (c *Context) Render(code int, render render.Render, obj ...interface{}) { - if err := render.Render(c.Writer, code, obj...); err != nil { - c.ErrorTyped(err, ErrorTypeInternal, obj) - c.AbortWithStatus(500) +// Intelligent shortcut for c.Writer.Header().Set(key, value) +// it writes a header in the response. +// If value == "", this method removes the header `c.Writer.Header().Del(key)` +func (c *Context) Header(key, value string) { + if len(value) == 0 { + c.Writer.Header().Del(key) + } else { + c.Writer.Header().Set(key, value) } } -// Serializes the given struct as JSON into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/json". -func (c *Context) JSON(code int, obj interface{}) { - c.Render(code, render.JSON, obj) +func (c *Context) Render(code int, r render.Render) { + c.writermem.WriteHeader(code) + if err := r.Render(c.Writer); err != nil { + c.renderError(err) + } } -// Serializes the given struct as XML into the response body in a fast and efficient way. -// It also sets the Content-Type as "application/xml". -func (c *Context) XML(code int, obj interface{}) { - c.Render(code, render.XML, obj) +func (c *Context) renderError(err error) { + debugPrintError(err) + c.AbortWithError(500, err).SetType(ErrorTypeRender) } // Renders the HTTP template specified by its file name. // It also updates the HTTP code and sets the Content-Type as "text/html". // See http://golang.org/doc/articles/wiki/ func (c *Context) HTML(code int, name string, obj interface{}) { - c.Render(code, c.Engine.HTMLRender, name, obj) + instance := c.engine.HTMLRender.Instance(name, obj) + c.Render(code, instance) } -// Writes the given string into the response body and sets the Content-Type to "text/plain". +// Serializes the given struct as pretty JSON (indented + endlines) into the response body. +// It also sets the Content-Type as "application/json". +// WARNING: we recommend to use this only for development propuses since printing pretty JSON is +// more CPU and bandwidth consuming. Use Context.JSON() instead. +func (c *Context) IndentedJSON(code int, obj interface{}) { + c.Render(code, render.IndentedJSON{Data: obj}) +} + +// Serializes the given struct as JSON into the response body. +// It also sets the Content-Type as "application/json". +func (c *Context) JSON(code int, obj interface{}) { + c.writermem.WriteHeader(code) + if err := render.WriteJSON(c.Writer, obj); err != nil { + c.renderError(err) + } +} + +// Serializes the given struct as XML into the response body. +// It also sets the Content-Type as "application/xml". +func (c *Context) XML(code int, obj interface{}) { + c.Render(code, render.XML{Data: obj}) +} + +// Writes the given string into the response body. func (c *Context) String(code int, format string, values ...interface{}) { - c.Render(code, render.Plain, format, values) + c.writermem.WriteHeader(code) + render.WriteString(c.Writer, format, values) } // Returns a HTTP redirect to the specific location. func (c *Context) Redirect(code int, location string) { - if code >= 300 && code <= 308 { - c.Render(code, render.Redirect, location) - } else { - panic(fmt.Sprintf("Cannot send a redirect with status code %d", code)) - } + c.Render(-1, render.Redirect{ + Code: code, + Location: location, + Request: c.Request, + }) } // Writes some data into the body stream and updates the HTTP code. func (c *Context) Data(code int, contentType string, data []byte) { - if len(contentType) > 0 { - c.Writer.Header().Set("Content-Type", contentType) - } - c.Writer.WriteHeader(code) - c.Writer.Write(data) + c.Render(code, render.Data{ + ContentType: contentType, + Data: data, + }) } -// Writes the specified file into the body stream +// Writes the specified file into the body stream in a efficient way. func (c *Context) File(filepath string) { http.ServeFile(c.Writer, c.Request, filepath) } +func (c *Context) SSEvent(name string, message interface{}) { + c.Render(-1, sse.Event{ + Event: name, + Data: message, + }) +} + +func (c *Context) Stream(step func(w io.Writer) bool) { + w := c.Writer + clientGone := w.CloseNotify() + for { + select { + case <-clientGone: + return + default: + keepopen := step(w) + w.Flush() + if !keepopen { + return + } + } + } +} + /************************************/ /******** CONTENT NEGOTIATION *******/ /************************************/ type Negotiate struct { Offered []string - HTMLPath string + HTMLName string HTMLData interface{} JSONData interface{} XMLData interface{} @@ -316,23 +404,20 @@ type Negotiate struct { func (c *Context) Negotiate(code int, config Negotiate) { switch c.NegotiateFormat(config.Offered...) { - case MIMEJSON: + case binding.MIMEJSON: data := chooseData(config.JSONData, config.Data) c.JSON(code, data) - case MIMEHTML: + case binding.MIMEHTML: data := chooseData(config.HTMLData, config.Data) - if len(config.HTMLPath) == 0 { - panic("negotiate config is wrong. html path is needed") - } - c.HTML(code, config.HTMLPath, data) + c.HTML(code, config.HTMLName, data) - case MIMEXML: + case binding.MIMEXML: data := chooseData(config.XMLData, config.Data) c.XML(code, data) default: - c.Fail(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) + c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) } } @@ -340,24 +425,49 @@ func (c *Context) NegotiateFormat(offered ...string) string { if len(offered) == 0 { panic("you must provide at least one offer") } - if c.accepted == nil { - c.accepted = parseAccept(c.Request.Header.Get("Accept")) + if c.Accepted == nil { + c.Accepted = parseAccept(c.requestHeader("Accept")) } - if len(c.accepted) == 0 { + if len(c.Accepted) == 0 { return offered[0] - - } else { - for _, accepted := range c.accepted { - for _, offert := range offered { - if accepted == offert { - return offert - } + } + for _, accepted := range c.Accepted { + for _, offert := range offered { + if accepted == offert { + return offert } } - return "" } + return "" } func (c *Context) SetAccepted(formats ...string) { - c.accepted = formats + c.Accepted = formats +} + +/************************************/ +/***** GOLANG.ORG/X/NET/CONTEXT *****/ +/************************************/ + +func (c *Context) Deadline() (deadline time.Time, ok bool) { + return +} + +func (c *Context) Done() <-chan struct{} { + return nil +} + +func (c *Context) Err() error { + return nil +} + +func (c *Context) Value(key interface{}) interface{} { + if key == 0 { + return c.Request + } + if keyAsString, ok := key.(string); ok { + val, _ := c.Get(keyAsString) + return val + } + return nil } diff --git a/context_test.go b/context_test.go index 8435ac56..a638d6dd 100644 --- a/context_test.go +++ b/context_test.go @@ -8,435 +8,548 @@ import ( "bytes" "errors" "html/template" + "mime/multipart" "net/http" "net/http/httptest" "testing" + "time" + + "github.com/manucorporat/sse" + "github.com/stretchr/testify/assert" ) -// TestContextParamsGet tests that a parameter can be parsed from the URL. -func TestContextParamsByName(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/alexandernyquist", nil) - w := httptest.NewRecorder() - name := "" +// Unit tests TODO +// func (c *Context) File(filepath string) { +// func (c *Context) Negotiate(code int, config Negotiate) { +// BAD case: func (c *Context) Render(code int, render render.Render, obj ...interface{}) { +// test that information is not leaked when reusing Contexts (using the Pool) - r := New() - r.GET("/test/:name", func(c *Context) { - name = c.Params.ByName("name") - }) +func createTestContext() (c *Context, w *httptest.ResponseRecorder, r *Engine) { + w = httptest.NewRecorder() + r = New() + c = r.allocateContext() + c.reset() + c.writermem.reset(w) + return +} - r.ServeHTTP(w, req) +func createMultipartRequest() *http.Request { + boundary := "--testboundary" + body := new(bytes.Buffer) + mw := multipart.NewWriter(body) + defer mw.Close() - if name != "alexandernyquist" { - t.Errorf("Url parameter was not correctly parsed. Should be alexandernyquist, was %s.", name) + must(mw.SetBoundary(boundary)) + must(mw.WriteField("foo", "bar")) + must(mw.WriteField("bar", "foo")) + req, err := http.NewRequest("POST", "/", body) + must(err) + req.Header.Set("Content-Type", MIMEMultipartPOSTForm+"; boundary="+boundary) + return req +} + +func must(err error) { + if err != nil { + panic(err.Error()) } } +func TestContextReset(t *testing.T) { + router := New() + c := router.allocateContext() + assert.Equal(t, c.engine, router) + + c.index = 2 + c.Writer = &responseWriter{ResponseWriter: httptest.NewRecorder()} + c.Params = Params{Param{}} + c.Error(errors.New("test")) + c.Set("foo", "bar") + c.reset() + + assert.False(t, c.IsAborted()) + assert.Nil(t, c.Keys) + assert.Nil(t, c.Accepted) + assert.Len(t, c.Errors, 0) + assert.Empty(t, c.Errors.Errors()) + assert.Empty(t, c.Errors.ByType(ErrorTypeAny)) + assert.Len(t, c.Params, 0) + assert.EqualValues(t, c.index, -1) + assert.Equal(t, c.Writer.(*responseWriter), &c.writermem) +} + // TestContextSetGet tests that a parameter is set correctly on the // current context and can be retrieved using Get. func TestContextSetGet(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() + c, _, _ := createTestContext() + c.Set("foo", "bar") - r := New() - r.GET("/test", func(c *Context) { - // Key should be lazily created - if c.Keys != nil { - t.Error("Keys should be nil") - } + value, err := c.Get("foo") + assert.Equal(t, value, "bar") + assert.True(t, err) - // Set - c.Set("foo", "bar") + value, err = c.Get("foo2") + assert.Nil(t, value) + assert.False(t, err) - v, err := c.Get("foo") - if err != nil { - t.Errorf("Error on exist key") - } - if v != "bar" { - t.Errorf("Value should be bar, was %s", v) - } - }) - - r.ServeHTTP(w, req) + assert.Equal(t, c.MustGet("foo"), "bar") + assert.Panics(t, func() { c.MustGet("no_exist") }) } -// TestContextJSON tests that the response is serialized as JSON +func TestContextSetGetValues(t *testing.T) { + c, _, _ := createTestContext() + c.Set("string", "this is a string") + c.Set("int32", int32(-42)) + c.Set("int64", int64(42424242424242)) + c.Set("uint64", uint64(42)) + c.Set("float32", float32(4.2)) + c.Set("float64", 4.2) + var a interface{} = 1 + c.Set("intInterface", a) + + assert.Exactly(t, c.MustGet("string").(string), "this is a string") + assert.Exactly(t, c.MustGet("int32").(int32), int32(-42)) + assert.Exactly(t, c.MustGet("int64").(int64), int64(42424242424242)) + assert.Exactly(t, c.MustGet("uint64").(uint64), uint64(42)) + assert.Exactly(t, c.MustGet("float32").(float32), float32(4.2)) + assert.Exactly(t, c.MustGet("float64").(float64), 4.2) + assert.Exactly(t, c.MustGet("intInterface").(int), 1) + +} + +func TestContextCopy(t *testing.T) { + c, _, _ := createTestContext() + c.index = 2 + c.Request, _ = http.NewRequest("POST", "/hola", nil) + c.handlers = HandlersChain{func(c *Context) {}} + c.Params = Params{Param{Key: "foo", Value: "bar"}} + c.Set("foo", "bar") + + cp := c.Copy() + assert.Nil(t, cp.handlers) + assert.Nil(t, cp.writermem.ResponseWriter) + assert.Equal(t, &cp.writermem, cp.Writer.(*responseWriter)) + assert.Equal(t, cp.Request, c.Request) + assert.Equal(t, cp.index, AbortIndex) + assert.Equal(t, cp.Keys, c.Keys) + assert.Equal(t, cp.engine, c.engine) + assert.Equal(t, cp.Params, c.Params) +} + +func TestContextQuery(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("GET", "http://example.com/?foo=bar&page=10", nil) + + assert.Equal(t, c.DefaultQuery("foo", "none"), "bar") + assert.Equal(t, c.Query("foo"), "bar") + assert.Empty(t, c.PostForm("foo")) + + assert.Equal(t, c.DefaultQuery("page", "0"), "10") + assert.Equal(t, c.Query("page"), "10") + assert.Empty(t, c.PostForm("page")) + + assert.Equal(t, c.DefaultQuery("NoKey", "nada"), "nada") + assert.Empty(t, c.Query("NoKey")) + assert.Empty(t, c.PostForm("NoKey")) +} + +func TestContextQueryAndPostForm(t *testing.T) { + c, _, _ := createTestContext() + body := bytes.NewBufferString("foo=bar&page=11&both=POST") + c.Request, _ = http.NewRequest("POST", "/?both=GET&id=main", body) + c.Request.Header.Add("Content-Type", MIMEPOSTForm) + + assert.Equal(t, c.DefaultPostForm("foo", "none"), "bar") + assert.Equal(t, c.PostForm("foo"), "bar") + assert.Empty(t, c.Query("foo")) + + assert.Equal(t, c.DefaultPostForm("page", "0"), "11") + assert.Equal(t, c.PostForm("page"), "11") + assert.Equal(t, c.Query("page"), "") + + assert.Equal(t, c.PostForm("both"), "POST") + assert.Equal(t, c.Query("both"), "GET") + + assert.Equal(t, c.DefaultPostForm("id", "000"), "000") + assert.Equal(t, c.Query("id"), "main") + assert.Empty(t, c.PostForm("id")) + + assert.Equal(t, c.DefaultPostForm("NoKey", "nada"), "nada") + assert.Empty(t, c.PostForm("NoKey")) + assert.Empty(t, c.Query("NoKey")) + + var obj struct { + Foo string `form:"foo"` + Id string `form:"id"` + Page string `form:"page"` + Both string `form:"both"` + } + assert.NoError(t, c.Bind(&obj)) + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, obj.Id, "main") + assert.Equal(t, obj.Page, "11") + assert.Equal(t, obj.Both, "POST") +} + +func TestContextPostFormMultipart(t *testing.T) { + c, _, _ := createTestContext() + c.Request = createMultipartRequest() + + var obj struct { + Foo string `form:"foo"` + Bar string `form:"bar"` + } + assert.NoError(t, c.Bind(&obj)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + + assert.Empty(t, c.Query("foo")) + assert.Empty(t, c.Query("bar")) + assert.Equal(t, c.PostForm("foo"), "bar") + assert.Equal(t, c.PostForm("bar"), "foo") +} + +// Tests that the response is serialized as JSON // and Content-Type is set to application/json -func TestContextJSON(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderJSON(t *testing.T) { + c, w, _ := createTestContext() + c.JSON(201, H{"foo": "bar"}) - r := New() - r.GET("/test", func(c *Context) { - c.JSON(200, H{"foo": "bar"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "{\"foo\":\"bar\"}\n" { - t.Errorf("Response should be {\"foo\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") } -// TestContextHTML tests that the response executes the templates +// Tests that the response is serialized as JSON +// we change the content-type before +func TestContextRenderAPIJSON(t *testing.T) { + c, w, _ := createTestContext() + c.Header("Content-Type", "application/vnd.api+json") + c.JSON(201, H{"foo": "bar"}) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/vnd.api+json") +} + +// Tests that the response is serialized as JSON +// and Content-Type is set to application/json +func TestContextRenderIndentedJSON(t *testing.T) { + c, w, _ := createTestContext() + c.IndentedJSON(201, H{"foo": "bar", "bar": "foo", "nested": H{"foo": "bar"}}) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\",\n \"nested\": {\n \"foo\": \"bar\"\n }\n}") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/json; charset=utf-8") +} + +// Tests that the response executes the templates // and responds with Content-Type set to text/html -func TestContextHTML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderHTML(t *testing.T) { + c, w, router := createTestContext() + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + router.SetHTMLTemplate(templ) - r := New() - templ, _ := template.New("t").Parse(`Hello {{.Name}}`) - r.SetHTMLTemplate(templ) + c.HTML(201, "t", H{"name": "alexandernyquist"}) - type TestData struct{ Name string } - - r.GET("/test", func(c *Context) { - c.HTML(200, "t", TestData{"alexandernyquist"}) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "Hello alexandernyquist" { - t.Errorf("Response should be Hello alexandernyquist, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/html" { - t.Errorf("Content-Type should be text/html, was %s", w.HeaderMap.Get("Content-Type")) - } -} - -// TestContextString tests that the response is returned -// with Content-Type set to text/plain -func TestContextString(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test", func(c *Context) { - c.String(200, "test") - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "test" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/plain" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "Hello alexandernyquist") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestContextXML tests that the response is serialized as XML // and Content-Type is set to application/xml -func TestContextXML(t *testing.T) { - req, _ := http.NewRequest("GET", "/test", nil) - w := httptest.NewRecorder() +func TestContextRenderXML(t *testing.T) { + c, w, _ := createTestContext() + c.XML(201, H{"foo": "bar"}) - r := New() - r.GET("/test", func(c *Context) { - c.XML(200, H{"foo": "bar"}) - }) + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "bar") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "application/xml; charset=utf-8") +} - r.ServeHTTP(w, req) +// TestContextString tests that the response is returned +// with Content-Type set to text/plain +func TestContextRenderString(t *testing.T) { + c, w, _ := createTestContext() + c.String(201, "test %s %d", "string", 2) - if w.Body.String() != "bar" { - t.Errorf("Response should be bar, was: %s", w.Body.String()) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "test string 2") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") +} - if w.HeaderMap.Get("Content-Type") != "application/xml" { - t.Errorf("Content-Type should be application/xml, was %s", w.HeaderMap.Get("Content-Type")) - } +// TestContextString tests that the response is returned +// with Content-Type set to text/html +func TestContextRenderHTMLString(t *testing.T) { + c, w, _ := createTestContext() + c.Header("Content-Type", "text/html; charset=utf-8") + c.String(201, "%s %d", "string", 3) + + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "string 3") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") } // TestContextData tests that the response can be written from `bytesting` // with specified MIME type -func TestContextData(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/csv", nil) - w := httptest.NewRecorder() +func TestContextRenderData(t *testing.T) { + c, w, _ := createTestContext() + c.Data(201, "text/csv", []byte(`foo,bar`)) - r := New() - r.GET("/test/csv", func(c *Context) { - c.Data(200, "text/csv", []byte(`foo,bar`)) - }) - - r.ServeHTTP(w, req) - - if w.Body.String() != "foo,bar" { - t.Errorf("Response should be foo&bar, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "text/csv" { - t.Errorf("Content-Type should be text/csv, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Code, 201) + assert.Equal(t, w.Body.String(), "foo,bar") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/csv") } -func TestContextFile(t *testing.T) { - req, _ := http.NewRequest("GET", "/test/file", nil) - w := httptest.NewRecorder() - - r := New() - r.GET("/test/file", func(c *Context) { - c.File("./gin.go") +func TestContextRenderSSE(t *testing.T) { + c, w, _ := createTestContext() + c.SSEvent("float", 1.5) + c.Render(-1, sse.Event{ + Id: "123", + Data: "text", + }) + c.SSEvent("chat", H{ + "foo": "bar", + "bar": "foo", }) - r.ServeHTTP(w, req) - - bodyAsString := w.Body.String() - - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file data") - } - - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain; charset=utf-8, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, w.Body.String(), "event: float\ndata: 1.5\n\nid: 123\ndata: text\n\nevent: chat\ndata: {\"bar\":\"foo\",\"foo\":\"bar\"}\n\n") } -// TestHandlerFunc - ensure that custom middleware works properly -func TestHandlerFunc(t *testing.T) { +func TestContextRenderFile(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("GET", "/", nil) + c.File("./gin.go") - req, _ := http.NewRequest("GET", "/", nil) - w := httptest.NewRecorder() - - r := New() - var stepsPassed int = 0 - - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) - - r.ServeHTTP(w, req) - - if w.Code != 404 { - t.Errorf("Response code should be Not found, was: %s", w.Code) - } - - if stepsPassed != 2 { - t.Errorf("Falied to switch context in handler function: %s", stepsPassed) - } + assert.Equal(t, w.Code, 200) + assert.Contains(t, w.Body.String(), "func New() *Engine {") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") } -// TestBadAbortHandlersChain - ensure that Abort after switch context will not interrupt pending handlers -func TestBadAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - // after check and abort - c.AbortWithStatus(409) - }) - r.Use(func(c *Context) { - stepsPassed += 1 - c.Next() - stepsPassed += 1 - c.AbortWithStatus(403) - }) +func TestContextHeaders(t *testing.T) { + c, _, _ := createTestContext() + c.Header("Content-Type", "text/plain") + c.Header("X-Custom", "value") - // RUN - w := PerformRequest(r, "GET", "/") + assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/plain") + assert.Equal(t, c.Writer.Header().Get("X-Custom"), "value") - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Forbiden, was: %d", w.Code) - } - if stepsPassed != 4 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } + c.Header("Content-Type", "text/html") + c.Header("X-Custom", "") + + assert.Equal(t, c.Writer.Header().Get("Content-Type"), "text/html") + _, exist := c.Writer.Header()["X-Custom"] + assert.False(t, exist) } -// TestAbortHandlersChain - ensure that Abort interrupt used middlewares in fifo order -func TestAbortHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.AbortWithStatus(409) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) +// TODO +func TestContextRenderRedirectWithRelativePath(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) + assert.Panics(t, func() { c.Redirect(299, "/new_path") }) + assert.Panics(t, func() { c.Redirect(309, "/new_path") }) - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 409 { - t.Errorf("Response code should be Conflict, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } + c.Redirect(302, "/path") + c.Writer.WriteHeaderNow() + assert.Equal(t, w.Code, 302) + assert.Equal(t, w.Header().Get("Location"), "/path") } -// TestFailHandlersChain - ensure that Fail interrupt used middlewares in fifo order as -// as well as Abort -func TestFailHandlersChain(t *testing.T) { - // SETUP - var stepsPassed int = 0 - r := New() - r.Use(func(context *Context) { - stepsPassed += 1 - context.Fail(500, errors.New("foo")) - }) - r.Use(func(context *Context) { - stepsPassed += 1 - context.Next() - stepsPassed += 1 - }) +func TestContextRenderRedirectWithAbsolutePath(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", nil) + c.Redirect(302, "http://google.com") + c.Writer.WriteHeaderNow() - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - if w.Code != 500 { - t.Errorf("Response code should be Server error, was: %d", w.Code) - } - if stepsPassed != 1 { - t.Errorf("Falied to switch context in handler function: %d", stepsPassed) - } + assert.Equal(t, w.Code, 302) + assert.Equal(t, w.Header().Get("Location"), "http://google.com") } -func TestBindingJSON(t *testing.T) { +func TestContextNegotiationFormat(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "", nil) - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Panics(t, func() { c.NegotiateFormat() }) + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) + assert.Equal(t, c.NegotiateFormat(MIMEHTML, MIMEJSON), MIMEHTML) } -func TestBindingJSONEncoding(t *testing.T) { +func TestContextNegotiationFormatWithAccept(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - body := bytes.NewBuffer([]byte("{\"foo\":\"嘉\"}")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json; charset=utf-8") - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) - } - - if w.Body.String() != "{\"parsed\":\"嘉\"}\n" { - t.Errorf("Response should be {\"parsed\":\"嘉\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") != "application/json" { - t.Errorf("Content-Type should be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEXML) + assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEHTML) + assert.Equal(t, c.NegotiateFormat(MIMEJSON), "") } -func TestBindingJSONNoContentType(t *testing.T) { +func TestContextNegotiationFormatCustum(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Add("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8") - body := bytes.NewBuffer([]byte("{\"foo\":\"bar\"}")) + c.Accepted = nil + c.SetAccepted(MIMEJSON, MIMEXML) - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) - } - - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, c.NegotiateFormat(MIMEJSON, MIMEXML), MIMEJSON) + assert.Equal(t, c.NegotiateFormat(MIMEXML, MIMEHTML), MIMEXML) + assert.Equal(t, c.NegotiateFormat(MIMEJSON), MIMEJSON) } -func TestBindingJSONMalformed(t *testing.T) { +// TestContextData tests that the response can be written from `bytesting` +// with specified MIME type +func TestContextAbortWithStatus(t *testing.T) { + c, w, _ := createTestContext() + c.index = 4 + c.AbortWithStatus(401) + c.Writer.WriteHeaderNow() - body := bytes.NewBuffer([]byte("\"foo\":\"bar\"\n")) - - r := New() - r.POST("/binding/json", func(c *Context) { - var body struct { - Foo string `json:"foo"` - } - if c.Bind(&body) { - c.JSON(200, H{"parsed": body.Foo}) - } - - }) - - req, _ := http.NewRequest("POST", "/binding/json", body) - req.Header.Set("Content-Type", "application/json") - - w := httptest.NewRecorder() - - r.ServeHTTP(w, req) - - if w.Code != 400 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) - } - if w.Body.String() == "{\"parsed\":\"bar\"}\n" { - t.Errorf("Response should not be {\"parsed\":\"bar\"}, was: %s", w.Body.String()) - } - - if w.HeaderMap.Get("Content-Type") == "application/json" { - t.Errorf("Content-Type should not be application/json, was %s", w.HeaderMap.Get("Content-Type")) - } + assert.Equal(t, c.index, AbortIndex) + assert.Equal(t, c.Writer.Status(), 401) + assert.Equal(t, w.Code, 401) + assert.True(t, c.IsAborted()) +} + +func TestContextError(t *testing.T) { + c, _, _ := createTestContext() + assert.Empty(t, c.Errors) + + c.Error(errors.New("first error")) + assert.Len(t, c.Errors, 1) + assert.Equal(t, c.Errors.String(), "Error #01: first error\n") + + c.Error(&Error{ + Err: errors.New("second error"), + Meta: "some data 2", + Type: ErrorTypePublic, + }) + assert.Len(t, c.Errors, 2) + + assert.Equal(t, c.Errors[0].Err, errors.New("first error")) + assert.Nil(t, c.Errors[0].Meta) + assert.Equal(t, c.Errors[0].Type, ErrorTypePrivate) + + assert.Equal(t, c.Errors[1].Err, errors.New("second error")) + assert.Equal(t, c.Errors[1].Meta, "some data 2") + assert.Equal(t, c.Errors[1].Type, ErrorTypePublic) + + assert.Equal(t, c.Errors.Last(), c.Errors[1]) +} + +func TestContextTypedError(t *testing.T) { + c, _, _ := createTestContext() + c.Error(errors.New("externo 0")).SetType(ErrorTypePublic) + c.Error(errors.New("interno 0")).SetType(ErrorTypePrivate) + + for _, err := range c.Errors.ByType(ErrorTypePublic) { + assert.Equal(t, err.Type, ErrorTypePublic) + } + for _, err := range c.Errors.ByType(ErrorTypePrivate) { + assert.Equal(t, err.Type, ErrorTypePrivate) + } + assert.Equal(t, c.Errors.Errors(), []string{"externo 0", "interno 0"}) +} + +func TestContextAbortWithError(t *testing.T) { + c, w, _ := createTestContext() + c.AbortWithError(401, errors.New("bad input")).SetMeta("some input") + c.Writer.WriteHeaderNow() + + assert.Equal(t, w.Code, 401) + assert.Equal(t, c.index, AbortIndex) + assert.True(t, c.IsAborted()) +} + +func TestContextClientIP(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "/", nil) + + c.Request.Header.Set("X-Real-IP", " 10.10.10.10 ") + c.Request.Header.Set("X-Forwarded-For", " 20.20.20.20, 30.30.30.30") + c.Request.RemoteAddr = " 40.40.40.40 " + + assert.Equal(t, c.ClientIP(), "10.10.10.10") + + c.Request.Header.Del("X-Real-IP") + assert.Equal(t, c.ClientIP(), "20.20.20.20") + + c.Request.Header.Set("X-Forwarded-For", "30.30.30.30 ") + assert.Equal(t, c.ClientIP(), "30.30.30.30") + + c.Request.Header.Del("X-Forwarded-For") + assert.Equal(t, c.ClientIP(), "40.40.40.40") +} + +func TestContextContentType(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "/", nil) + c.Request.Header.Set("Content-Type", "application/json; charset=utf-8") + + assert.Equal(t, c.ContentType(), "application/json") +} + +func TestContextAutoBindJSON(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + assert.NoError(t, c.Bind(&obj)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Empty(t, c.Errors) +} + +func TestContextBindWithJSON(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type + + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + assert.NoError(t, c.BindJSON(&obj)) + assert.Equal(t, obj.Bar, "foo") + assert.Equal(t, obj.Foo, "bar") + assert.Equal(t, w.Body.Len(), 0) +} + +func TestContextBadAutoBind(t *testing.T) { + c, w, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "http://example.com", bytes.NewBufferString("\"foo\":\"bar\", \"bar\":\"foo\"}")) + c.Request.Header.Add("Content-Type", MIMEJSON) + var obj struct { + Foo string `json:"foo"` + Bar string `json:"bar"` + } + + assert.False(t, c.IsAborted()) + assert.Error(t, c.Bind(&obj)) + c.Writer.WriteHeaderNow() + + assert.Empty(t, obj.Bar) + assert.Empty(t, obj.Foo) + assert.Equal(t, w.Code, 400) + assert.True(t, c.IsAborted()) +} + +func TestContextGolangContext(t *testing.T) { + c, _, _ := createTestContext() + c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString("{\"foo\":\"bar\", \"bar\":\"foo\"}")) + assert.NoError(t, c.Err()) + assert.Nil(t, c.Done()) + ti, ok := c.Deadline() + assert.Equal(t, ti, time.Time{}) + assert.False(t, ok) + assert.Equal(t, c.Value(0), c.Request) + assert.Nil(t, c.Value("foo")) + + c.Set("foo", "bar") + assert.Equal(t, c.Value("foo"), "bar") + assert.Nil(t, c.Value(1)) } diff --git a/debug.go b/debug.go new file mode 100644 index 00000000..a0b99f43 --- /dev/null +++ b/debug.go @@ -0,0 +1,42 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import "log" + +func init() { + log.SetFlags(0) +} +func IsDebugging() bool { + return ginMode == debugCode +} + +func debugPrintRoute(httpMethod, absolutePath string, handlers HandlersChain) { + if IsDebugging() { + nuHandlers := len(handlers) + handlerName := nameOfFunction(handlers[nuHandlers-1]) + debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) + } +} + +func debugPrint(format string, values ...interface{}) { + if IsDebugging() { + log.Printf("[GIN-debug] "+format, values...) + } +} + +func debugPrintWARNING() { + debugPrint(`[WARNING] Running in "debug" mode. Switch to "release" mode in production. + - using env: export GIN_MODE=release + - using code: gin.SetMode(gin.ReleaseMode) + +`) +} + +func debugPrintError(err error) { + if err != nil { + debugPrint("[ERROR] %v\n", err) + } +} diff --git a/debug_test.go b/debug_test.go new file mode 100644 index 00000000..425aff0f --- /dev/null +++ b/debug_test.go @@ -0,0 +1,68 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "errors" + "io" + "log" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TODO +// func debugRoute(httpMethod, absolutePath string, handlers HandlersChain) { +// func debugPrint(format string, values ...interface{}) { + +func TestIsDebugging(t *testing.T) { + SetMode(DebugMode) + assert.True(t, IsDebugging()) + SetMode(ReleaseMode) + assert.False(t, IsDebugging()) + SetMode(TestMode) + assert.False(t, IsDebugging()) +} + +func TestDebugPrint(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + SetMode(ReleaseMode) + debugPrint("DEBUG this!") + SetMode(TestMode) + debugPrint("DEBUG this!") + assert.Empty(t, w.String()) + + SetMode(DebugMode) + debugPrint("these are %d %s\n", 2, "error messages") + assert.Equal(t, w.String(), "[GIN-debug] these are 2 error messages\n") +} + +func TestDebugPrintError(t *testing.T) { + var w bytes.Buffer + setup(&w) + defer teardown() + + SetMode(DebugMode) + debugPrintError(nil) + assert.Empty(t, w.String()) + + debugPrintError(errors.New("this is an error")) + assert.Equal(t, w.String(), "[GIN-debug] [ERROR] this is an error\n") +} + +func setup(w io.Writer) { + SetMode(DebugMode) + log.SetOutput(w) +} + +func teardown() { + SetMode(TestMode) + log.SetOutput(os.Stdout) +} diff --git a/deprecated.go b/deprecated.go index eb248dde..b2e874f0 100644 --- a/deprecated.go +++ b/deprecated.go @@ -3,45 +3,3 @@ // license that can be found in the LICENSE file. package gin - -import ( - "github.com/gin-gonic/gin/binding" - "net/http" -) - -// DEPRECATED, use Bind() instead. -// Like ParseBody() but this method also writes a 400 error if the json is not valid. -func (c *Context) EnsureBody(item interface{}) bool { - return c.Bind(item) -} - -// DEPRECATED use bindings directly -// Parses the body content as a JSON input. It decodes the json payload into the struct specified as a pointer. -func (c *Context) ParseBody(item interface{}) error { - return binding.JSON.Bind(c.Request, item) -} - -// DEPRECATED use gin.Static() instead -// ServeFiles serves files from the given file system root. -// The path must end with "/*filepath", files are then served from the local -// path /defined/root/dir/*filepath. -// For example if root is "/etc" and *filepath is "passwd", the local file -// "/etc/passwd" would be served. -// Internally a http.FileServer is used, therefore http.NotFound is used instead -// of the Router's NotFound handler. -// To use the operating system's file system implementation, -// use http.Dir: -// router.ServeFiles("/src/*filepath", http.Dir("/var/www")) -func (engine *Engine) ServeFiles(path string, root http.FileSystem) { - engine.router.ServeFiles(path, root) -} - -// DEPRECATED use gin.LoadHTMLGlob() or gin.LoadHTMLFiles() instead -func (engine *Engine) LoadHTMLTemplates(pattern string) { - engine.LoadHTMLGlob(pattern) -} - -// DEPRECATED. Use NotFound() instead -func (engine *Engine) NotFound404(handlers ...HandlerFunc) { - engine.NoRoute(handlers...) -} diff --git a/errors.go b/errors.go new file mode 100644 index 00000000..982c0267 --- /dev/null +++ b/errors.go @@ -0,0 +1,161 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "encoding/json" + "fmt" + "reflect" +) + +type ErrorType uint64 + +const ( + ErrorTypeBind ErrorType = 1 << 63 // used when c.Bind() fails + ErrorTypeRender ErrorType = 1 << 62 // used when c.Render() fails + ErrorTypePrivate ErrorType = 1 << 0 + ErrorTypePublic ErrorType = 1 << 1 + + ErrorTypeAny ErrorType = 1<<64 - 1 + ErrorTypeNu = 2 +) + +type ( + Error struct { + Err error + Type ErrorType + Meta interface{} + } + + errorMsgs []*Error +) + +var _ error = &Error{} + +func (msg *Error) SetType(flags ErrorType) *Error { + msg.Type = flags + return msg +} + +func (msg *Error) SetMeta(data interface{}) *Error { + msg.Meta = data + return msg +} + +func (msg *Error) JSON() interface{} { + json := H{} + if msg.Meta != nil { + value := reflect.ValueOf(msg.Meta) + switch value.Kind() { + case reflect.Struct: + return msg.Meta + case reflect.Map: + for _, key := range value.MapKeys() { + json[key.String()] = value.MapIndex(key).Interface() + } + default: + json["meta"] = msg.Meta + } + } + if _, ok := json["error"]; !ok { + json["error"] = msg.Error() + } + return json +} + +// Implements the json.Marshaller interface +func (msg *Error) MarshalJSON() ([]byte, error) { + return json.Marshal(msg.JSON()) +} + +// Implements the error interface +func (msg *Error) Error() string { + return msg.Err.Error() +} + +func (msg *Error) IsType(flags ErrorType) bool { + return (msg.Type & flags) > 0 +} + +// Returns a readonly copy filterd the byte. +// ie ByType(gin.ErrorTypePublic) returns a slice of errors with type=ErrorTypePublic +func (a errorMsgs) ByType(typ ErrorType) errorMsgs { + if len(a) == 0 { + return nil + } + if typ == ErrorTypeAny { + return a + } + var result errorMsgs = nil + for _, msg := range a { + if msg.IsType(typ) { + result = append(result, msg) + } + } + return result +} + +// Returns the last error in the slice. It returns nil if the array is empty. +// Shortcut for errors[len(errors)-1] +func (a errorMsgs) Last() *Error { + length := len(a) + if length == 0 { + return nil + } + return a[length-1] +} + +// Returns an array will all the error messages. +// Example +// ``` +// c.Error(errors.New("first")) +// c.Error(errors.New("second")) +// c.Error(errors.New("third")) +// c.Errors.Errors() // == []string{"first", "second", "third"} +// `` +func (a errorMsgs) Errors() []string { + if len(a) == 0 { + return nil + } + errorStrings := make([]string, len(a)) + for i, err := range a { + errorStrings[i] = err.Error() + } + return errorStrings +} + +func (a errorMsgs) JSON() interface{} { + switch len(a) { + case 0: + return nil + case 1: + return a.Last().JSON() + default: + json := make([]interface{}, len(a)) + for i, err := range a { + json[i] = err.JSON() + } + return json + } +} + +func (a errorMsgs) MarshalJSON() ([]byte, error) { + return json.Marshal(a.JSON()) +} + +func (a errorMsgs) String() string { + if len(a) == 0 { + return "" + } + var buffer bytes.Buffer + for i, msg := range a { + fmt.Fprintf(&buffer, "Error #%02d: %s\n", (i + 1), msg.Err) + if msg.Meta != nil { + fmt.Fprintf(&buffer, " Meta: %v\n", msg.Meta) + } + } + return buffer.String() +} diff --git a/errors_test.go b/errors_test.go new file mode 100644 index 00000000..748e3fe0 --- /dev/null +++ b/errors_test.go @@ -0,0 +1,98 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "encoding/json" + "errors" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestError(t *testing.T) { + baseError := errors.New("test error") + err := &Error{ + Err: baseError, + Type: ErrorTypePrivate, + } + assert.Equal(t, err.Error(), baseError.Error()) + assert.Equal(t, err.JSON(), H{"error": baseError.Error()}) + + assert.Equal(t, err.SetType(ErrorTypePublic), err) + assert.Equal(t, err.Type, ErrorTypePublic) + + assert.Equal(t, err.SetMeta("some data"), err) + assert.Equal(t, err.Meta, "some data") + assert.Equal(t, err.JSON(), H{ + "error": baseError.Error(), + "meta": "some data", + }) + + jsonBytes, _ := json.Marshal(err) + assert.Equal(t, string(jsonBytes), "{\"error\":\"test error\",\"meta\":\"some data\"}") + + err.SetMeta(H{ + "status": "200", + "data": "some data", + }) + assert.Equal(t, err.JSON(), H{ + "error": baseError.Error(), + "status": "200", + "data": "some data", + }) + + err.SetMeta(H{ + "error": "custom error", + "status": "200", + "data": "some data", + }) + assert.Equal(t, err.JSON(), H{ + "error": "custom error", + "status": "200", + "data": "some data", + }) +} + +func TestErrorSlice(t *testing.T) { + errs := errorMsgs{ + {Err: errors.New("first"), Type: ErrorTypePrivate}, + {Err: errors.New("second"), Type: ErrorTypePrivate, Meta: "some data"}, + {Err: errors.New("third"), Type: ErrorTypePublic, Meta: H{"status": "400"}}, + } + + assert.Equal(t, errs.Last().Error(), "third") + assert.Equal(t, errs.Errors(), []string{"first", "second", "third"}) + assert.Equal(t, errs.ByType(ErrorTypePublic).Errors(), []string{"third"}) + assert.Equal(t, errs.ByType(ErrorTypePrivate).Errors(), []string{"first", "second"}) + assert.Equal(t, errs.ByType(ErrorTypePublic|ErrorTypePrivate).Errors(), []string{"first", "second", "third"}) + assert.Empty(t, errs.ByType(ErrorTypeBind)) + assert.Empty(t, errs.ByType(ErrorTypeBind).String()) + + assert.Equal(t, errs.String(), `Error #01: first +Error #02: second + Meta: some data +Error #03: third + Meta: map[status:400] +`) + assert.Equal(t, errs.JSON(), []interface{}{ + H{"error": "first"}, + H{"error": "second", "meta": "some data"}, + H{"error": "third", "status": "400"}, + }) + jsonBytes, _ := json.Marshal(errs) + assert.Equal(t, string(jsonBytes), "[{\"error\":\"first\"},{\"error\":\"second\",\"meta\":\"some data\"},{\"error\":\"third\",\"status\":\"400\"}]") + errs = errorMsgs{ + {Err: errors.New("first"), Type: ErrorTypePrivate}, + } + assert.Equal(t, errs.JSON(), H{"error": "first"}) + jsonBytes, _ = json.Marshal(errs) + assert.Equal(t, string(jsonBytes), "{\"error\":\"first\"}") + + errs = errorMsgs{} + assert.Nil(t, errs.Last()) + assert.Nil(t, errs.JSON()) + assert.Empty(t, errs.String()) +} diff --git a/examples/example_basic.go b/examples/basic/main.go similarity index 97% rename from examples/example_basic.go rename to examples/basic/main.go index 919580d8..80f2bd3c 100644 --- a/examples/example_basic.go +++ b/examples/basic/main.go @@ -45,7 +45,7 @@ func main() { Value string `json:"value" binding:"required"` } - if c.Bind(&json) { + if c.Bind(&json) == nil { DB[user] = json.Value c.JSON(200, gin.H{"status": "ok"}) } diff --git a/examples/realtime-advanced/main.go b/examples/realtime-advanced/main.go new file mode 100644 index 00000000..1f3c8585 --- /dev/null +++ b/examples/realtime-advanced/main.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "runtime" + + "github.com/gin-gonic/gin" +) + +func main() { + ConfigRuntime() + StartWorkers() + StartGin() +} + +func ConfigRuntime() { + nuCPU := runtime.NumCPU() + runtime.GOMAXPROCS(nuCPU) + fmt.Printf("Running with %d CPUs\n", nuCPU) +} + +func StartWorkers() { + go statsWorker() +} + +func StartGin() { + gin.SetMode(gin.ReleaseMode) + + router := gin.New() + router.Use(rateLimit, gin.Recovery()) + router.LoadHTMLGlob("resources/*.templ.html") + router.Static("/static", "resources/static") + router.GET("/", index) + router.GET("/room/:roomid", roomGET) + router.POST("/room-post/:roomid", roomPOST) + router.GET("/stream/:roomid", streamRoom) + + router.Run(":80") +} diff --git a/examples/realtime-advanced/resources/room_login.templ.html b/examples/realtime-advanced/resources/room_login.templ.html new file mode 100644 index 00000000..27dac387 --- /dev/null +++ b/examples/realtime-advanced/resources/room_login.templ.html @@ -0,0 +1,208 @@ + + + + + + + Server-Sent Events. Room "{{.roomid}}" + + + + + + + + + + + + + + + + + + + + + + + +
+
+

Server-Sent Events in Go

+

Server-sent events (SSE) is a technology where a browser receives automatic updates from a server via HTTP connection. It is not websockets. Learn more.

+

The chat and the charts data is provided in realtime using the SSE implemention of Gin Framework.

+
+
+
+ + + + + + + + +
NickMessage
+
+ {{if .nick}} +
+
+ +
+
{{.nick}}
+ +
+
+ +
+ {{else}} +
+ Join the SSE real-time chat +
+ +
+
+ +
+
+ {{end}} +
+
+
+

+ ◼︎ Users
+ ◼︎ Inbound messages / sec
+ ◼︎ Outbound messages / sec
+

+
+
+
+
+
+
+

Realtime server Go stats

+
+

Memory usage

+

+

+

+

+ ◼︎ Heap bytes
+ ◼︎ Stack bytes
+

+
+
+

Allocations per second

+

+

+

+

+ ◼︎ Mallocs / sec
+ ◼︎ Frees / sec
+

+
+
+
+

MIT Open Sourced

+ +
+ +

Server-side (Go)

+
func streamRoom(c *gin.Context) {
+    roomid := c.ParamValue("roomid")
+    listener := openListener(roomid)
+    statsTicker := time.NewTicker(1 * time.Second)
+    defer closeListener(roomid, listener)
+    defer statsTicker.Stop()
+
+    c.Stream(func(w io.Writer) bool {
+        select {
+        case msg := <-listener:
+            c.SSEvent("message", msg)
+        case <-statsTicker.C:
+            c.SSEvent("stats", Stats())
+        }
+        return true
+    })
+}
+
+
+

Client-side (JS)

+
function StartSSE(roomid) {
+    var source = new EventSource('/stream/'+roomid);
+    source.addEventListener('message', newChatMessage, false);
+    source.addEventListener('stats', stats, false);
+}
+
+
+
+
+

SSE package

+
import "github.com/manucorporat/sse"
+
+func httpHandler(w http.ResponseWriter, req *http.Request) {
+    // data can be a primitive like a string, an integer or a float
+    sse.Encode(w, sse.Event{
+        Event: "message",
+        Data:  "some data\nmore data",
+    })
+
+    // also a complex type, like a map, a struct or a slice
+    sse.Encode(w, sse.Event{
+        Id:    "124",
+        Event: "message",
+        Data: map[string]interface{}{
+            "user":    "manu",
+            "date":    time.Now().Unix(),
+            "content": "hi!",
+        },
+    })
+}
+
event: message
+data: some data\\nmore data
+
+id: 124
+event: message
+data: {"content":"hi!","date":1431540810,"user":"manu"}
+
+
+
+ +
+ + diff --git a/examples/realtime-advanced/resources/static/epoch.min.css b/examples/realtime-advanced/resources/static/epoch.min.css new file mode 100644 index 00000000..47a80cdc --- /dev/null +++ b/examples/realtime-advanced/resources/static/epoch.min.css @@ -0,0 +1 @@ +.epoch .axis path,.epoch .axis line{shape-rendering:crispEdges;}.epoch .axis.canvas .tick line{shape-rendering:geometricPrecision;}div#_canvas_css_reference{width:0;height:0;position:absolute;top:-1000px;left:-1000px;}div#_canvas_css_reference svg{position:absolute;width:0;height:0;top:-1000px;left:-1000px;}.epoch{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:12pt;}.epoch .axis path,.epoch .axis line{fill:none;stroke:#000;}.epoch .axis .tick text{font-size:9pt;}.epoch .line{fill:none;stroke-width:2px;}.epoch.sparklines .line{stroke-width:1px;}.epoch .area{stroke:none;}.epoch .arc.pie{stroke:#fff;stroke-width:1.5px;}.epoch .arc.pie text{stroke:none;fill:white;font-size:9pt;}.epoch .gauge-labels .value{text-anchor:middle;font-size:140%;fill:#666;}.epoch.gauge-tiny{width:120px;height:90px;}.epoch.gauge-tiny .gauge-labels .value{font-size:80%;}.epoch.gauge-tiny .gauge .arc.outer{stroke-width:2px;}.epoch.gauge-small{width:180px;height:135px;}.epoch.gauge-small .gauge-labels .value{font-size:120%;}.epoch.gauge-small .gauge .arc.outer{stroke-width:3px;}.epoch.gauge-medium{width:240px;height:180px;}.epoch.gauge-medium .gauge .arc.outer{stroke-width:3px;}.epoch.gauge-large{width:320px;height:240px;}.epoch.gauge-large .gauge-labels .value{font-size:180%;}.epoch .gauge .arc.outer{stroke-width:4px;stroke:#666;}.epoch .gauge .arc.inner{stroke-width:1px;stroke:#555;}.epoch .gauge .tick{stroke-width:1px;stroke:#555;}.epoch .gauge .needle{fill:orange;}.epoch .gauge .needle-base{fill:#666;}.epoch div.ref.category1,.epoch.category10 div.ref.category1{background-color:#1f77b4;}.epoch .category1 .line,.epoch.category10 .category1 .line{stroke:#1f77b4;}.epoch .category1 .area,.epoch .category1 .dot,.epoch.category10 .category1 .area,.epoch.category10 .category1 .dot{fill:#1f77b4;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category1 path,.epoch.category10 .arc.category1 path{fill:#1f77b4;}.epoch .bar.category1,.epoch.category10 .bar.category1{fill:#1f77b4;}.epoch div.ref.category2,.epoch.category10 div.ref.category2{background-color:#ff7f0e;}.epoch .category2 .line,.epoch.category10 .category2 .line{stroke:#ff7f0e;}.epoch .category2 .area,.epoch .category2 .dot,.epoch.category10 .category2 .area,.epoch.category10 .category2 .dot{fill:#ff7f0e;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category2 path,.epoch.category10 .arc.category2 path{fill:#ff7f0e;}.epoch .bar.category2,.epoch.category10 .bar.category2{fill:#ff7f0e;}.epoch div.ref.category3,.epoch.category10 div.ref.category3{background-color:#2ca02c;}.epoch .category3 .line,.epoch.category10 .category3 .line{stroke:#2ca02c;}.epoch .category3 .area,.epoch .category3 .dot,.epoch.category10 .category3 .area,.epoch.category10 .category3 .dot{fill:#2ca02c;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category3 path,.epoch.category10 .arc.category3 path{fill:#2ca02c;}.epoch .bar.category3,.epoch.category10 .bar.category3{fill:#2ca02c;}.epoch div.ref.category4,.epoch.category10 div.ref.category4{background-color:#d62728;}.epoch .category4 .line,.epoch.category10 .category4 .line{stroke:#d62728;}.epoch .category4 .area,.epoch .category4 .dot,.epoch.category10 .category4 .area,.epoch.category10 .category4 .dot{fill:#d62728;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category4 path,.epoch.category10 .arc.category4 path{fill:#d62728;}.epoch .bar.category4,.epoch.category10 .bar.category4{fill:#d62728;}.epoch div.ref.category5,.epoch.category10 div.ref.category5{background-color:#9467bd;}.epoch .category5 .line,.epoch.category10 .category5 .line{stroke:#9467bd;}.epoch .category5 .area,.epoch .category5 .dot,.epoch.category10 .category5 .area,.epoch.category10 .category5 .dot{fill:#9467bd;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category5 path,.epoch.category10 .arc.category5 path{fill:#9467bd;}.epoch .bar.category5,.epoch.category10 .bar.category5{fill:#9467bd;}.epoch div.ref.category6,.epoch.category10 div.ref.category6{background-color:#8c564b;}.epoch .category6 .line,.epoch.category10 .category6 .line{stroke:#8c564b;}.epoch .category6 .area,.epoch .category6 .dot,.epoch.category10 .category6 .area,.epoch.category10 .category6 .dot{fill:#8c564b;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category6 path,.epoch.category10 .arc.category6 path{fill:#8c564b;}.epoch .bar.category6,.epoch.category10 .bar.category6{fill:#8c564b;}.epoch div.ref.category7,.epoch.category10 div.ref.category7{background-color:#e377c2;}.epoch .category7 .line,.epoch.category10 .category7 .line{stroke:#e377c2;}.epoch .category7 .area,.epoch .category7 .dot,.epoch.category10 .category7 .area,.epoch.category10 .category7 .dot{fill:#e377c2;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category7 path,.epoch.category10 .arc.category7 path{fill:#e377c2;}.epoch .bar.category7,.epoch.category10 .bar.category7{fill:#e377c2;}.epoch div.ref.category8,.epoch.category10 div.ref.category8{background-color:#7f7f7f;}.epoch .category8 .line,.epoch.category10 .category8 .line{stroke:#7f7f7f;}.epoch .category8 .area,.epoch .category8 .dot,.epoch.category10 .category8 .area,.epoch.category10 .category8 .dot{fill:#7f7f7f;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category8 path,.epoch.category10 .arc.category8 path{fill:#7f7f7f;}.epoch .bar.category8,.epoch.category10 .bar.category8{fill:#7f7f7f;}.epoch div.ref.category9,.epoch.category10 div.ref.category9{background-color:#bcbd22;}.epoch .category9 .line,.epoch.category10 .category9 .line{stroke:#bcbd22;}.epoch .category9 .area,.epoch .category9 .dot,.epoch.category10 .category9 .area,.epoch.category10 .category9 .dot{fill:#bcbd22;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category9 path,.epoch.category10 .arc.category9 path{fill:#bcbd22;}.epoch .bar.category9,.epoch.category10 .bar.category9{fill:#bcbd22;}.epoch div.ref.category10,.epoch.category10 div.ref.category10{background-color:#17becf;}.epoch .category10 .line,.epoch.category10 .category10 .line{stroke:#17becf;}.epoch .category10 .area,.epoch .category10 .dot,.epoch.category10 .category10 .area,.epoch.category10 .category10 .dot{fill:#17becf;stroke:rgba(0, 0, 0, 0);}.epoch .arc.category10 path,.epoch.category10 .arc.category10 path{fill:#17becf;}.epoch .bar.category10,.epoch.category10 .bar.category10{fill:#17becf;}.epoch.category20 div.ref.category1{background-color:#1f77b4;}.epoch.category20 .category1 .line{stroke:#1f77b4;}.epoch.category20 .category1 .area,.epoch.category20 .category1 .dot{fill:#1f77b4;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category1 path{fill:#1f77b4;}.epoch.category20 .bar.category1{fill:#1f77b4;}.epoch.category20 div.ref.category2{background-color:#aec7e8;}.epoch.category20 .category2 .line{stroke:#aec7e8;}.epoch.category20 .category2 .area,.epoch.category20 .category2 .dot{fill:#aec7e8;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category2 path{fill:#aec7e8;}.epoch.category20 .bar.category2{fill:#aec7e8;}.epoch.category20 div.ref.category3{background-color:#ff7f0e;}.epoch.category20 .category3 .line{stroke:#ff7f0e;}.epoch.category20 .category3 .area,.epoch.category20 .category3 .dot{fill:#ff7f0e;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category3 path{fill:#ff7f0e;}.epoch.category20 .bar.category3{fill:#ff7f0e;}.epoch.category20 div.ref.category4{background-color:#ffbb78;}.epoch.category20 .category4 .line{stroke:#ffbb78;}.epoch.category20 .category4 .area,.epoch.category20 .category4 .dot{fill:#ffbb78;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category4 path{fill:#ffbb78;}.epoch.category20 .bar.category4{fill:#ffbb78;}.epoch.category20 div.ref.category5{background-color:#2ca02c;}.epoch.category20 .category5 .line{stroke:#2ca02c;}.epoch.category20 .category5 .area,.epoch.category20 .category5 .dot{fill:#2ca02c;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category5 path{fill:#2ca02c;}.epoch.category20 .bar.category5{fill:#2ca02c;}.epoch.category20 div.ref.category6{background-color:#98df8a;}.epoch.category20 .category6 .line{stroke:#98df8a;}.epoch.category20 .category6 .area,.epoch.category20 .category6 .dot{fill:#98df8a;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category6 path{fill:#98df8a;}.epoch.category20 .bar.category6{fill:#98df8a;}.epoch.category20 div.ref.category7{background-color:#d62728;}.epoch.category20 .category7 .line{stroke:#d62728;}.epoch.category20 .category7 .area,.epoch.category20 .category7 .dot{fill:#d62728;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category7 path{fill:#d62728;}.epoch.category20 .bar.category7{fill:#d62728;}.epoch.category20 div.ref.category8{background-color:#ff9896;}.epoch.category20 .category8 .line{stroke:#ff9896;}.epoch.category20 .category8 .area,.epoch.category20 .category8 .dot{fill:#ff9896;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category8 path{fill:#ff9896;}.epoch.category20 .bar.category8{fill:#ff9896;}.epoch.category20 div.ref.category9{background-color:#9467bd;}.epoch.category20 .category9 .line{stroke:#9467bd;}.epoch.category20 .category9 .area,.epoch.category20 .category9 .dot{fill:#9467bd;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category9 path{fill:#9467bd;}.epoch.category20 .bar.category9{fill:#9467bd;}.epoch.category20 div.ref.category10{background-color:#c5b0d5;}.epoch.category20 .category10 .line{stroke:#c5b0d5;}.epoch.category20 .category10 .area,.epoch.category20 .category10 .dot{fill:#c5b0d5;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category10 path{fill:#c5b0d5;}.epoch.category20 .bar.category10{fill:#c5b0d5;}.epoch.category20 div.ref.category11{background-color:#8c564b;}.epoch.category20 .category11 .line{stroke:#8c564b;}.epoch.category20 .category11 .area,.epoch.category20 .category11 .dot{fill:#8c564b;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category11 path{fill:#8c564b;}.epoch.category20 .bar.category11{fill:#8c564b;}.epoch.category20 div.ref.category12{background-color:#c49c94;}.epoch.category20 .category12 .line{stroke:#c49c94;}.epoch.category20 .category12 .area,.epoch.category20 .category12 .dot{fill:#c49c94;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category12 path{fill:#c49c94;}.epoch.category20 .bar.category12{fill:#c49c94;}.epoch.category20 div.ref.category13{background-color:#e377c2;}.epoch.category20 .category13 .line{stroke:#e377c2;}.epoch.category20 .category13 .area,.epoch.category20 .category13 .dot{fill:#e377c2;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category13 path{fill:#e377c2;}.epoch.category20 .bar.category13{fill:#e377c2;}.epoch.category20 div.ref.category14{background-color:#f7b6d2;}.epoch.category20 .category14 .line{stroke:#f7b6d2;}.epoch.category20 .category14 .area,.epoch.category20 .category14 .dot{fill:#f7b6d2;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category14 path{fill:#f7b6d2;}.epoch.category20 .bar.category14{fill:#f7b6d2;}.epoch.category20 div.ref.category15{background-color:#7f7f7f;}.epoch.category20 .category15 .line{stroke:#7f7f7f;}.epoch.category20 .category15 .area,.epoch.category20 .category15 .dot{fill:#7f7f7f;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category15 path{fill:#7f7f7f;}.epoch.category20 .bar.category15{fill:#7f7f7f;}.epoch.category20 div.ref.category16{background-color:#c7c7c7;}.epoch.category20 .category16 .line{stroke:#c7c7c7;}.epoch.category20 .category16 .area,.epoch.category20 .category16 .dot{fill:#c7c7c7;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category16 path{fill:#c7c7c7;}.epoch.category20 .bar.category16{fill:#c7c7c7;}.epoch.category20 div.ref.category17{background-color:#bcbd22;}.epoch.category20 .category17 .line{stroke:#bcbd22;}.epoch.category20 .category17 .area,.epoch.category20 .category17 .dot{fill:#bcbd22;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category17 path{fill:#bcbd22;}.epoch.category20 .bar.category17{fill:#bcbd22;}.epoch.category20 div.ref.category18{background-color:#dbdb8d;}.epoch.category20 .category18 .line{stroke:#dbdb8d;}.epoch.category20 .category18 .area,.epoch.category20 .category18 .dot{fill:#dbdb8d;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category18 path{fill:#dbdb8d;}.epoch.category20 .bar.category18{fill:#dbdb8d;}.epoch.category20 div.ref.category19{background-color:#17becf;}.epoch.category20 .category19 .line{stroke:#17becf;}.epoch.category20 .category19 .area,.epoch.category20 .category19 .dot{fill:#17becf;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category19 path{fill:#17becf;}.epoch.category20 .bar.category19{fill:#17becf;}.epoch.category20 div.ref.category20{background-color:#9edae5;}.epoch.category20 .category20 .line{stroke:#9edae5;}.epoch.category20 .category20 .area,.epoch.category20 .category20 .dot{fill:#9edae5;stroke:rgba(0, 0, 0, 0);}.epoch.category20 .arc.category20 path{fill:#9edae5;}.epoch.category20 .bar.category20{fill:#9edae5;}.epoch.category20b div.ref.category1{background-color:#393b79;}.epoch.category20b .category1 .line{stroke:#393b79;}.epoch.category20b .category1 .area,.epoch.category20b .category1 .dot{fill:#393b79;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category1 path{fill:#393b79;}.epoch.category20b .bar.category1{fill:#393b79;}.epoch.category20b div.ref.category2{background-color:#5254a3;}.epoch.category20b .category2 .line{stroke:#5254a3;}.epoch.category20b .category2 .area,.epoch.category20b .category2 .dot{fill:#5254a3;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category2 path{fill:#5254a3;}.epoch.category20b .bar.category2{fill:#5254a3;}.epoch.category20b div.ref.category3{background-color:#6b6ecf;}.epoch.category20b .category3 .line{stroke:#6b6ecf;}.epoch.category20b .category3 .area,.epoch.category20b .category3 .dot{fill:#6b6ecf;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category3 path{fill:#6b6ecf;}.epoch.category20b .bar.category3{fill:#6b6ecf;}.epoch.category20b div.ref.category4{background-color:#9c9ede;}.epoch.category20b .category4 .line{stroke:#9c9ede;}.epoch.category20b .category4 .area,.epoch.category20b .category4 .dot{fill:#9c9ede;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category4 path{fill:#9c9ede;}.epoch.category20b .bar.category4{fill:#9c9ede;}.epoch.category20b div.ref.category5{background-color:#637939;}.epoch.category20b .category5 .line{stroke:#637939;}.epoch.category20b .category5 .area,.epoch.category20b .category5 .dot{fill:#637939;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category5 path{fill:#637939;}.epoch.category20b .bar.category5{fill:#637939;}.epoch.category20b div.ref.category6{background-color:#8ca252;}.epoch.category20b .category6 .line{stroke:#8ca252;}.epoch.category20b .category6 .area,.epoch.category20b .category6 .dot{fill:#8ca252;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category6 path{fill:#8ca252;}.epoch.category20b .bar.category6{fill:#8ca252;}.epoch.category20b div.ref.category7{background-color:#b5cf6b;}.epoch.category20b .category7 .line{stroke:#b5cf6b;}.epoch.category20b .category7 .area,.epoch.category20b .category7 .dot{fill:#b5cf6b;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category7 path{fill:#b5cf6b;}.epoch.category20b .bar.category7{fill:#b5cf6b;}.epoch.category20b div.ref.category8{background-color:#cedb9c;}.epoch.category20b .category8 .line{stroke:#cedb9c;}.epoch.category20b .category8 .area,.epoch.category20b .category8 .dot{fill:#cedb9c;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category8 path{fill:#cedb9c;}.epoch.category20b .bar.category8{fill:#cedb9c;}.epoch.category20b div.ref.category9{background-color:#8c6d31;}.epoch.category20b .category9 .line{stroke:#8c6d31;}.epoch.category20b .category9 .area,.epoch.category20b .category9 .dot{fill:#8c6d31;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category9 path{fill:#8c6d31;}.epoch.category20b .bar.category9{fill:#8c6d31;}.epoch.category20b div.ref.category10{background-color:#bd9e39;}.epoch.category20b .category10 .line{stroke:#bd9e39;}.epoch.category20b .category10 .area,.epoch.category20b .category10 .dot{fill:#bd9e39;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category10 path{fill:#bd9e39;}.epoch.category20b .bar.category10{fill:#bd9e39;}.epoch.category20b div.ref.category11{background-color:#e7ba52;}.epoch.category20b .category11 .line{stroke:#e7ba52;}.epoch.category20b .category11 .area,.epoch.category20b .category11 .dot{fill:#e7ba52;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category11 path{fill:#e7ba52;}.epoch.category20b .bar.category11{fill:#e7ba52;}.epoch.category20b div.ref.category12{background-color:#e7cb94;}.epoch.category20b .category12 .line{stroke:#e7cb94;}.epoch.category20b .category12 .area,.epoch.category20b .category12 .dot{fill:#e7cb94;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category12 path{fill:#e7cb94;}.epoch.category20b .bar.category12{fill:#e7cb94;}.epoch.category20b div.ref.category13{background-color:#843c39;}.epoch.category20b .category13 .line{stroke:#843c39;}.epoch.category20b .category13 .area,.epoch.category20b .category13 .dot{fill:#843c39;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category13 path{fill:#843c39;}.epoch.category20b .bar.category13{fill:#843c39;}.epoch.category20b div.ref.category14{background-color:#ad494a;}.epoch.category20b .category14 .line{stroke:#ad494a;}.epoch.category20b .category14 .area,.epoch.category20b .category14 .dot{fill:#ad494a;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category14 path{fill:#ad494a;}.epoch.category20b .bar.category14{fill:#ad494a;}.epoch.category20b div.ref.category15{background-color:#d6616b;}.epoch.category20b .category15 .line{stroke:#d6616b;}.epoch.category20b .category15 .area,.epoch.category20b .category15 .dot{fill:#d6616b;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category15 path{fill:#d6616b;}.epoch.category20b .bar.category15{fill:#d6616b;}.epoch.category20b div.ref.category16{background-color:#e7969c;}.epoch.category20b .category16 .line{stroke:#e7969c;}.epoch.category20b .category16 .area,.epoch.category20b .category16 .dot{fill:#e7969c;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category16 path{fill:#e7969c;}.epoch.category20b .bar.category16{fill:#e7969c;}.epoch.category20b div.ref.category17{background-color:#7b4173;}.epoch.category20b .category17 .line{stroke:#7b4173;}.epoch.category20b .category17 .area,.epoch.category20b .category17 .dot{fill:#7b4173;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category17 path{fill:#7b4173;}.epoch.category20b .bar.category17{fill:#7b4173;}.epoch.category20b div.ref.category18{background-color:#a55194;}.epoch.category20b .category18 .line{stroke:#a55194;}.epoch.category20b .category18 .area,.epoch.category20b .category18 .dot{fill:#a55194;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category18 path{fill:#a55194;}.epoch.category20b .bar.category18{fill:#a55194;}.epoch.category20b div.ref.category19{background-color:#ce6dbd;}.epoch.category20b .category19 .line{stroke:#ce6dbd;}.epoch.category20b .category19 .area,.epoch.category20b .category19 .dot{fill:#ce6dbd;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category19 path{fill:#ce6dbd;}.epoch.category20b .bar.category19{fill:#ce6dbd;}.epoch.category20b div.ref.category20{background-color:#de9ed6;}.epoch.category20b .category20 .line{stroke:#de9ed6;}.epoch.category20b .category20 .area,.epoch.category20b .category20 .dot{fill:#de9ed6;stroke:rgba(0, 0, 0, 0);}.epoch.category20b .arc.category20 path{fill:#de9ed6;}.epoch.category20b .bar.category20{fill:#de9ed6;}.epoch.category20c div.ref.category1{background-color:#3182bd;}.epoch.category20c .category1 .line{stroke:#3182bd;}.epoch.category20c .category1 .area,.epoch.category20c .category1 .dot{fill:#3182bd;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category1 path{fill:#3182bd;}.epoch.category20c .bar.category1{fill:#3182bd;}.epoch.category20c div.ref.category2{background-color:#6baed6;}.epoch.category20c .category2 .line{stroke:#6baed6;}.epoch.category20c .category2 .area,.epoch.category20c .category2 .dot{fill:#6baed6;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category2 path{fill:#6baed6;}.epoch.category20c .bar.category2{fill:#6baed6;}.epoch.category20c div.ref.category3{background-color:#9ecae1;}.epoch.category20c .category3 .line{stroke:#9ecae1;}.epoch.category20c .category3 .area,.epoch.category20c .category3 .dot{fill:#9ecae1;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category3 path{fill:#9ecae1;}.epoch.category20c .bar.category3{fill:#9ecae1;}.epoch.category20c div.ref.category4{background-color:#c6dbef;}.epoch.category20c .category4 .line{stroke:#c6dbef;}.epoch.category20c .category4 .area,.epoch.category20c .category4 .dot{fill:#c6dbef;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category4 path{fill:#c6dbef;}.epoch.category20c .bar.category4{fill:#c6dbef;}.epoch.category20c div.ref.category5{background-color:#e6550d;}.epoch.category20c .category5 .line{stroke:#e6550d;}.epoch.category20c .category5 .area,.epoch.category20c .category5 .dot{fill:#e6550d;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category5 path{fill:#e6550d;}.epoch.category20c .bar.category5{fill:#e6550d;}.epoch.category20c div.ref.category6{background-color:#fd8d3c;}.epoch.category20c .category6 .line{stroke:#fd8d3c;}.epoch.category20c .category6 .area,.epoch.category20c .category6 .dot{fill:#fd8d3c;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category6 path{fill:#fd8d3c;}.epoch.category20c .bar.category6{fill:#fd8d3c;}.epoch.category20c div.ref.category7{background-color:#fdae6b;}.epoch.category20c .category7 .line{stroke:#fdae6b;}.epoch.category20c .category7 .area,.epoch.category20c .category7 .dot{fill:#fdae6b;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category7 path{fill:#fdae6b;}.epoch.category20c .bar.category7{fill:#fdae6b;}.epoch.category20c div.ref.category8{background-color:#fdd0a2;}.epoch.category20c .category8 .line{stroke:#fdd0a2;}.epoch.category20c .category8 .area,.epoch.category20c .category8 .dot{fill:#fdd0a2;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category8 path{fill:#fdd0a2;}.epoch.category20c .bar.category8{fill:#fdd0a2;}.epoch.category20c div.ref.category9{background-color:#31a354;}.epoch.category20c .category9 .line{stroke:#31a354;}.epoch.category20c .category9 .area,.epoch.category20c .category9 .dot{fill:#31a354;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category9 path{fill:#31a354;}.epoch.category20c .bar.category9{fill:#31a354;}.epoch.category20c div.ref.category10{background-color:#74c476;}.epoch.category20c .category10 .line{stroke:#74c476;}.epoch.category20c .category10 .area,.epoch.category20c .category10 .dot{fill:#74c476;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category10 path{fill:#74c476;}.epoch.category20c .bar.category10{fill:#74c476;}.epoch.category20c div.ref.category11{background-color:#a1d99b;}.epoch.category20c .category11 .line{stroke:#a1d99b;}.epoch.category20c .category11 .area,.epoch.category20c .category11 .dot{fill:#a1d99b;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category11 path{fill:#a1d99b;}.epoch.category20c .bar.category11{fill:#a1d99b;}.epoch.category20c div.ref.category12{background-color:#c7e9c0;}.epoch.category20c .category12 .line{stroke:#c7e9c0;}.epoch.category20c .category12 .area,.epoch.category20c .category12 .dot{fill:#c7e9c0;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category12 path{fill:#c7e9c0;}.epoch.category20c .bar.category12{fill:#c7e9c0;}.epoch.category20c div.ref.category13{background-color:#756bb1;}.epoch.category20c .category13 .line{stroke:#756bb1;}.epoch.category20c .category13 .area,.epoch.category20c .category13 .dot{fill:#756bb1;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category13 path{fill:#756bb1;}.epoch.category20c .bar.category13{fill:#756bb1;}.epoch.category20c div.ref.category14{background-color:#9e9ac8;}.epoch.category20c .category14 .line{stroke:#9e9ac8;}.epoch.category20c .category14 .area,.epoch.category20c .category14 .dot{fill:#9e9ac8;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category14 path{fill:#9e9ac8;}.epoch.category20c .bar.category14{fill:#9e9ac8;}.epoch.category20c div.ref.category15{background-color:#bcbddc;}.epoch.category20c .category15 .line{stroke:#bcbddc;}.epoch.category20c .category15 .area,.epoch.category20c .category15 .dot{fill:#bcbddc;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category15 path{fill:#bcbddc;}.epoch.category20c .bar.category15{fill:#bcbddc;}.epoch.category20c div.ref.category16{background-color:#dadaeb;}.epoch.category20c .category16 .line{stroke:#dadaeb;}.epoch.category20c .category16 .area,.epoch.category20c .category16 .dot{fill:#dadaeb;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category16 path{fill:#dadaeb;}.epoch.category20c .bar.category16{fill:#dadaeb;}.epoch.category20c div.ref.category17{background-color:#636363;}.epoch.category20c .category17 .line{stroke:#636363;}.epoch.category20c .category17 .area,.epoch.category20c .category17 .dot{fill:#636363;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category17 path{fill:#636363;}.epoch.category20c .bar.category17{fill:#636363;}.epoch.category20c div.ref.category18{background-color:#969696;}.epoch.category20c .category18 .line{stroke:#969696;}.epoch.category20c .category18 .area,.epoch.category20c .category18 .dot{fill:#969696;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category18 path{fill:#969696;}.epoch.category20c .bar.category18{fill:#969696;}.epoch.category20c div.ref.category19{background-color:#bdbdbd;}.epoch.category20c .category19 .line{stroke:#bdbdbd;}.epoch.category20c .category19 .area,.epoch.category20c .category19 .dot{fill:#bdbdbd;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category19 path{fill:#bdbdbd;}.epoch.category20c .bar.category19{fill:#bdbdbd;}.epoch.category20c div.ref.category20{background-color:#d9d9d9;}.epoch.category20c .category20 .line{stroke:#d9d9d9;}.epoch.category20c .category20 .area,.epoch.category20c .category20 .dot{fill:#d9d9d9;stroke:rgba(0, 0, 0, 0);}.epoch.category20c .arc.category20 path{fill:#d9d9d9;}.epoch.category20c .bar.category20{fill:#d9d9d9;}.epoch .category1 .bucket,.epoch.heatmap5 .category1 .bucket{fill:#1f77b4;}.epoch .category2 .bucket,.epoch.heatmap5 .category2 .bucket{fill:#2ca02c;}.epoch .category3 .bucket,.epoch.heatmap5 .category3 .bucket{fill:#d62728;}.epoch .category4 .bucket,.epoch.heatmap5 .category4 .bucket{fill:#8c564b;}.epoch .category5 .bucket,.epoch.heatmap5 .category5 .bucket{fill:#7f7f7f;}.epoch-theme-dark .epoch .axis path,.epoch-theme-dark .epoch .axis line{stroke:#d0d0d0;}.epoch-theme-dark .epoch .axis .tick text{fill:#d0d0d0;}.epoch-theme-dark .arc.pie{stroke:#333;}.epoch-theme-dark .arc.pie text{fill:#333;}.epoch-theme-dark .epoch .gauge-labels .value{fill:#BBB;}.epoch-theme-dark .epoch .gauge .arc.outer{stroke:#999;}.epoch-theme-dark .epoch .gauge .arc.inner{stroke:#AAA;}.epoch-theme-dark .epoch .gauge .tick{stroke:#AAA;}.epoch-theme-dark .epoch .gauge .needle{fill:#F3DE88;}.epoch-theme-dark .epoch .gauge .needle-base{fill:#999;}.epoch-theme-dark .epoch div.ref.category1,.epoch-theme-dark .epoch.category10 div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch .category1 .line,.epoch-theme-dark .epoch.category10 .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch .category1 .area,.epoch-theme-dark .epoch .category1 .dot,.epoch-theme-dark .epoch.category10 .category1 .area,.epoch-theme-dark .epoch.category10 .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category1 path,.epoch-theme-dark .epoch.category10 .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch .bar.category1,.epoch-theme-dark .epoch.category10 .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch div.ref.category2,.epoch-theme-dark .epoch.category10 div.ref.category2{background-color:#FFAC89;}.epoch-theme-dark .epoch .category2 .line,.epoch-theme-dark .epoch.category10 .category2 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch .category2 .area,.epoch-theme-dark .epoch .category2 .dot,.epoch-theme-dark .epoch.category10 .category2 .area,.epoch-theme-dark .epoch.category10 .category2 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category2 path,.epoch-theme-dark .epoch.category10 .arc.category2 path{fill:#FFAC89;}.epoch-theme-dark .epoch .bar.category2,.epoch-theme-dark .epoch.category10 .bar.category2{fill:#FFAC89;}.epoch-theme-dark .epoch div.ref.category3,.epoch-theme-dark .epoch.category10 div.ref.category3{background-color:#E889E8;}.epoch-theme-dark .epoch .category3 .line,.epoch-theme-dark .epoch.category10 .category3 .line{stroke:#E889E8;}.epoch-theme-dark .epoch .category3 .area,.epoch-theme-dark .epoch .category3 .dot,.epoch-theme-dark .epoch.category10 .category3 .area,.epoch-theme-dark .epoch.category10 .category3 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category3 path,.epoch-theme-dark .epoch.category10 .arc.category3 path{fill:#E889E8;}.epoch-theme-dark .epoch .bar.category3,.epoch-theme-dark .epoch.category10 .bar.category3{fill:#E889E8;}.epoch-theme-dark .epoch div.ref.category4,.epoch-theme-dark .epoch.category10 div.ref.category4{background-color:#78E8D3;}.epoch-theme-dark .epoch .category4 .line,.epoch-theme-dark .epoch.category10 .category4 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch .category4 .area,.epoch-theme-dark .epoch .category4 .dot,.epoch-theme-dark .epoch.category10 .category4 .area,.epoch-theme-dark .epoch.category10 .category4 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category4 path,.epoch-theme-dark .epoch.category10 .arc.category4 path{fill:#78E8D3;}.epoch-theme-dark .epoch .bar.category4,.epoch-theme-dark .epoch.category10 .bar.category4{fill:#78E8D3;}.epoch-theme-dark .epoch div.ref.category5,.epoch-theme-dark .epoch.category10 div.ref.category5{background-color:#C2FF97;}.epoch-theme-dark .epoch .category5 .line,.epoch-theme-dark .epoch.category10 .category5 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch .category5 .area,.epoch-theme-dark .epoch .category5 .dot,.epoch-theme-dark .epoch.category10 .category5 .area,.epoch-theme-dark .epoch.category10 .category5 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category5 path,.epoch-theme-dark .epoch.category10 .arc.category5 path{fill:#C2FF97;}.epoch-theme-dark .epoch .bar.category5,.epoch-theme-dark .epoch.category10 .bar.category5{fill:#C2FF97;}.epoch-theme-dark .epoch div.ref.category6,.epoch-theme-dark .epoch.category10 div.ref.category6{background-color:#B7BCD1;}.epoch-theme-dark .epoch .category6 .line,.epoch-theme-dark .epoch.category10 .category6 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch .category6 .area,.epoch-theme-dark .epoch .category6 .dot,.epoch-theme-dark .epoch.category10 .category6 .area,.epoch-theme-dark .epoch.category10 .category6 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category6 path,.epoch-theme-dark .epoch.category10 .arc.category6 path{fill:#B7BCD1;}.epoch-theme-dark .epoch .bar.category6,.epoch-theme-dark .epoch.category10 .bar.category6{fill:#B7BCD1;}.epoch-theme-dark .epoch div.ref.category7,.epoch-theme-dark .epoch.category10 div.ref.category7{background-color:#FF857F;}.epoch-theme-dark .epoch .category7 .line,.epoch-theme-dark .epoch.category10 .category7 .line{stroke:#FF857F;}.epoch-theme-dark .epoch .category7 .area,.epoch-theme-dark .epoch .category7 .dot,.epoch-theme-dark .epoch.category10 .category7 .area,.epoch-theme-dark .epoch.category10 .category7 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category7 path,.epoch-theme-dark .epoch.category10 .arc.category7 path{fill:#FF857F;}.epoch-theme-dark .epoch .bar.category7,.epoch-theme-dark .epoch.category10 .bar.category7{fill:#FF857F;}.epoch-theme-dark .epoch div.ref.category8,.epoch-theme-dark .epoch.category10 div.ref.category8{background-color:#F3DE88;}.epoch-theme-dark .epoch .category8 .line,.epoch-theme-dark .epoch.category10 .category8 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch .category8 .area,.epoch-theme-dark .epoch .category8 .dot,.epoch-theme-dark .epoch.category10 .category8 .area,.epoch-theme-dark .epoch.category10 .category8 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category8 path,.epoch-theme-dark .epoch.category10 .arc.category8 path{fill:#F3DE88;}.epoch-theme-dark .epoch .bar.category8,.epoch-theme-dark .epoch.category10 .bar.category8{fill:#F3DE88;}.epoch-theme-dark .epoch div.ref.category9,.epoch-theme-dark .epoch.category10 div.ref.category9{background-color:#C9935E;}.epoch-theme-dark .epoch .category9 .line,.epoch-theme-dark .epoch.category10 .category9 .line{stroke:#C9935E;}.epoch-theme-dark .epoch .category9 .area,.epoch-theme-dark .epoch .category9 .dot,.epoch-theme-dark .epoch.category10 .category9 .area,.epoch-theme-dark .epoch.category10 .category9 .dot{fill:#C9935E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category9 path,.epoch-theme-dark .epoch.category10 .arc.category9 path{fill:#C9935E;}.epoch-theme-dark .epoch .bar.category9,.epoch-theme-dark .epoch.category10 .bar.category9{fill:#C9935E;}.epoch-theme-dark .epoch div.ref.category10,.epoch-theme-dark .epoch.category10 div.ref.category10{background-color:#A488FF;}.epoch-theme-dark .epoch .category10 .line,.epoch-theme-dark .epoch.category10 .category10 .line{stroke:#A488FF;}.epoch-theme-dark .epoch .category10 .area,.epoch-theme-dark .epoch .category10 .dot,.epoch-theme-dark .epoch.category10 .category10 .area,.epoch-theme-dark .epoch.category10 .category10 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch .arc.category10 path,.epoch-theme-dark .epoch.category10 .arc.category10 path{fill:#A488FF;}.epoch-theme-dark .epoch .bar.category10,.epoch-theme-dark .epoch.category10 .bar.category10{fill:#A488FF;}.epoch-theme-dark .epoch.category20 div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch.category20 .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch.category20 .category1 .area,.epoch-theme-dark .epoch.category20 .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch.category20 .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch.category20 div.ref.category2{background-color:#626AAD;}.epoch-theme-dark .epoch.category20 .category2 .line{stroke:#626AAD;}.epoch-theme-dark .epoch.category20 .category2 .area,.epoch-theme-dark .epoch.category20 .category2 .dot{fill:#626AAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category2 path{fill:#626AAD;}.epoch-theme-dark .epoch.category20 .bar.category2{fill:#626AAD;}.epoch-theme-dark .epoch.category20 div.ref.category3{background-color:#FFAC89;}.epoch-theme-dark .epoch.category20 .category3 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch.category20 .category3 .area,.epoch-theme-dark .epoch.category20 .category3 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category3 path{fill:#FFAC89;}.epoch-theme-dark .epoch.category20 .bar.category3{fill:#FFAC89;}.epoch-theme-dark .epoch.category20 div.ref.category4{background-color:#BD7F66;}.epoch-theme-dark .epoch.category20 .category4 .line{stroke:#BD7F66;}.epoch-theme-dark .epoch.category20 .category4 .area,.epoch-theme-dark .epoch.category20 .category4 .dot{fill:#BD7F66;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category4 path{fill:#BD7F66;}.epoch-theme-dark .epoch.category20 .bar.category4{fill:#BD7F66;}.epoch-theme-dark .epoch.category20 div.ref.category5{background-color:#E889E8;}.epoch-theme-dark .epoch.category20 .category5 .line{stroke:#E889E8;}.epoch-theme-dark .epoch.category20 .category5 .area,.epoch-theme-dark .epoch.category20 .category5 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category5 path{fill:#E889E8;}.epoch-theme-dark .epoch.category20 .bar.category5{fill:#E889E8;}.epoch-theme-dark .epoch.category20 div.ref.category6{background-color:#995A99;}.epoch-theme-dark .epoch.category20 .category6 .line{stroke:#995A99;}.epoch-theme-dark .epoch.category20 .category6 .area,.epoch-theme-dark .epoch.category20 .category6 .dot{fill:#995A99;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category6 path{fill:#995A99;}.epoch-theme-dark .epoch.category20 .bar.category6{fill:#995A99;}.epoch-theme-dark .epoch.category20 div.ref.category7{background-color:#78E8D3;}.epoch-theme-dark .epoch.category20 .category7 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch.category20 .category7 .area,.epoch-theme-dark .epoch.category20 .category7 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category7 path{fill:#78E8D3;}.epoch-theme-dark .epoch.category20 .bar.category7{fill:#78E8D3;}.epoch-theme-dark .epoch.category20 div.ref.category8{background-color:#4F998C;}.epoch-theme-dark .epoch.category20 .category8 .line{stroke:#4F998C;}.epoch-theme-dark .epoch.category20 .category8 .area,.epoch-theme-dark .epoch.category20 .category8 .dot{fill:#4F998C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category8 path{fill:#4F998C;}.epoch-theme-dark .epoch.category20 .bar.category8{fill:#4F998C;}.epoch-theme-dark .epoch.category20 div.ref.category9{background-color:#C2FF97;}.epoch-theme-dark .epoch.category20 .category9 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch.category20 .category9 .area,.epoch-theme-dark .epoch.category20 .category9 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category9 path{fill:#C2FF97;}.epoch-theme-dark .epoch.category20 .bar.category9{fill:#C2FF97;}.epoch-theme-dark .epoch.category20 div.ref.category10{background-color:#789E5E;}.epoch-theme-dark .epoch.category20 .category10 .line{stroke:#789E5E;}.epoch-theme-dark .epoch.category20 .category10 .area,.epoch-theme-dark .epoch.category20 .category10 .dot{fill:#789E5E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category10 path{fill:#789E5E;}.epoch-theme-dark .epoch.category20 .bar.category10{fill:#789E5E;}.epoch-theme-dark .epoch.category20 div.ref.category11{background-color:#B7BCD1;}.epoch-theme-dark .epoch.category20 .category11 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch.category20 .category11 .area,.epoch-theme-dark .epoch.category20 .category11 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category11 path{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20 .bar.category11{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20 div.ref.category12{background-color:#7F8391;}.epoch-theme-dark .epoch.category20 .category12 .line{stroke:#7F8391;}.epoch-theme-dark .epoch.category20 .category12 .area,.epoch-theme-dark .epoch.category20 .category12 .dot{fill:#7F8391;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category12 path{fill:#7F8391;}.epoch-theme-dark .epoch.category20 .bar.category12{fill:#7F8391;}.epoch-theme-dark .epoch.category20 div.ref.category13{background-color:#CCB889;}.epoch-theme-dark .epoch.category20 .category13 .line{stroke:#CCB889;}.epoch-theme-dark .epoch.category20 .category13 .area,.epoch-theme-dark .epoch.category20 .category13 .dot{fill:#CCB889;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category13 path{fill:#CCB889;}.epoch-theme-dark .epoch.category20 .bar.category13{fill:#CCB889;}.epoch-theme-dark .epoch.category20 div.ref.category14{background-color:#A1906B;}.epoch-theme-dark .epoch.category20 .category14 .line{stroke:#A1906B;}.epoch-theme-dark .epoch.category20 .category14 .area,.epoch-theme-dark .epoch.category20 .category14 .dot{fill:#A1906B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category14 path{fill:#A1906B;}.epoch-theme-dark .epoch.category20 .bar.category14{fill:#A1906B;}.epoch-theme-dark .epoch.category20 div.ref.category15{background-color:#F3DE88;}.epoch-theme-dark .epoch.category20 .category15 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch.category20 .category15 .area,.epoch-theme-dark .epoch.category20 .category15 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category15 path{fill:#F3DE88;}.epoch-theme-dark .epoch.category20 .bar.category15{fill:#F3DE88;}.epoch-theme-dark .epoch.category20 div.ref.category16{background-color:#A89A5E;}.epoch-theme-dark .epoch.category20 .category16 .line{stroke:#A89A5E;}.epoch-theme-dark .epoch.category20 .category16 .area,.epoch-theme-dark .epoch.category20 .category16 .dot{fill:#A89A5E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category16 path{fill:#A89A5E;}.epoch-theme-dark .epoch.category20 .bar.category16{fill:#A89A5E;}.epoch-theme-dark .epoch.category20 div.ref.category17{background-color:#FF857F;}.epoch-theme-dark .epoch.category20 .category17 .line{stroke:#FF857F;}.epoch-theme-dark .epoch.category20 .category17 .area,.epoch-theme-dark .epoch.category20 .category17 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category17 path{fill:#FF857F;}.epoch-theme-dark .epoch.category20 .bar.category17{fill:#FF857F;}.epoch-theme-dark .epoch.category20 div.ref.category18{background-color:#BA615D;}.epoch-theme-dark .epoch.category20 .category18 .line{stroke:#BA615D;}.epoch-theme-dark .epoch.category20 .category18 .area,.epoch-theme-dark .epoch.category20 .category18 .dot{fill:#BA615D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category18 path{fill:#BA615D;}.epoch-theme-dark .epoch.category20 .bar.category18{fill:#BA615D;}.epoch-theme-dark .epoch.category20 div.ref.category19{background-color:#A488FF;}.epoch-theme-dark .epoch.category20 .category19 .line{stroke:#A488FF;}.epoch-theme-dark .epoch.category20 .category19 .area,.epoch-theme-dark .epoch.category20 .category19 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category19 path{fill:#A488FF;}.epoch-theme-dark .epoch.category20 .bar.category19{fill:#A488FF;}.epoch-theme-dark .epoch.category20 div.ref.category20{background-color:#7662B8;}.epoch-theme-dark .epoch.category20 .category20 .line{stroke:#7662B8;}.epoch-theme-dark .epoch.category20 .category20 .area,.epoch-theme-dark .epoch.category20 .category20 .dot{fill:#7662B8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20 .arc.category20 path{fill:#7662B8;}.epoch-theme-dark .epoch.category20 .bar.category20{fill:#7662B8;}.epoch-theme-dark .epoch.category20b div.ref.category1{background-color:#909CFF;}.epoch-theme-dark .epoch.category20b .category1 .line{stroke:#909CFF;}.epoch-theme-dark .epoch.category20b .category1 .area,.epoch-theme-dark .epoch.category20b .category1 .dot{fill:#909CFF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category1 path{fill:#909CFF;}.epoch-theme-dark .epoch.category20b .bar.category1{fill:#909CFF;}.epoch-theme-dark .epoch.category20b div.ref.category2{background-color:#7680D1;}.epoch-theme-dark .epoch.category20b .category2 .line{stroke:#7680D1;}.epoch-theme-dark .epoch.category20b .category2 .area,.epoch-theme-dark .epoch.category20b .category2 .dot{fill:#7680D1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category2 path{fill:#7680D1;}.epoch-theme-dark .epoch.category20b .bar.category2{fill:#7680D1;}.epoch-theme-dark .epoch.category20b div.ref.category3{background-color:#656DB2;}.epoch-theme-dark .epoch.category20b .category3 .line{stroke:#656DB2;}.epoch-theme-dark .epoch.category20b .category3 .area,.epoch-theme-dark .epoch.category20b .category3 .dot{fill:#656DB2;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category3 path{fill:#656DB2;}.epoch-theme-dark .epoch.category20b .bar.category3{fill:#656DB2;}.epoch-theme-dark .epoch.category20b div.ref.category4{background-color:#525992;}.epoch-theme-dark .epoch.category20b .category4 .line{stroke:#525992;}.epoch-theme-dark .epoch.category20b .category4 .area,.epoch-theme-dark .epoch.category20b .category4 .dot{fill:#525992;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category4 path{fill:#525992;}.epoch-theme-dark .epoch.category20b .bar.category4{fill:#525992;}.epoch-theme-dark .epoch.category20b div.ref.category5{background-color:#FFAC89;}.epoch-theme-dark .epoch.category20b .category5 .line{stroke:#FFAC89;}.epoch-theme-dark .epoch.category20b .category5 .area,.epoch-theme-dark .epoch.category20b .category5 .dot{fill:#FFAC89;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category5 path{fill:#FFAC89;}.epoch-theme-dark .epoch.category20b .bar.category5{fill:#FFAC89;}.epoch-theme-dark .epoch.category20b div.ref.category6{background-color:#D18D71;}.epoch-theme-dark .epoch.category20b .category6 .line{stroke:#D18D71;}.epoch-theme-dark .epoch.category20b .category6 .area,.epoch-theme-dark .epoch.category20b .category6 .dot{fill:#D18D71;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category6 path{fill:#D18D71;}.epoch-theme-dark .epoch.category20b .bar.category6{fill:#D18D71;}.epoch-theme-dark .epoch.category20b div.ref.category7{background-color:#AB735C;}.epoch-theme-dark .epoch.category20b .category7 .line{stroke:#AB735C;}.epoch-theme-dark .epoch.category20b .category7 .area,.epoch-theme-dark .epoch.category20b .category7 .dot{fill:#AB735C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category7 path{fill:#AB735C;}.epoch-theme-dark .epoch.category20b .bar.category7{fill:#AB735C;}.epoch-theme-dark .epoch.category20b div.ref.category8{background-color:#92624E;}.epoch-theme-dark .epoch.category20b .category8 .line{stroke:#92624E;}.epoch-theme-dark .epoch.category20b .category8 .area,.epoch-theme-dark .epoch.category20b .category8 .dot{fill:#92624E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category8 path{fill:#92624E;}.epoch-theme-dark .epoch.category20b .bar.category8{fill:#92624E;}.epoch-theme-dark .epoch.category20b div.ref.category9{background-color:#E889E8;}.epoch-theme-dark .epoch.category20b .category9 .line{stroke:#E889E8;}.epoch-theme-dark .epoch.category20b .category9 .area,.epoch-theme-dark .epoch.category20b .category9 .dot{fill:#E889E8;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category9 path{fill:#E889E8;}.epoch-theme-dark .epoch.category20b .bar.category9{fill:#E889E8;}.epoch-theme-dark .epoch.category20b div.ref.category10{background-color:#BA6EBA;}.epoch-theme-dark .epoch.category20b .category10 .line{stroke:#BA6EBA;}.epoch-theme-dark .epoch.category20b .category10 .area,.epoch-theme-dark .epoch.category20b .category10 .dot{fill:#BA6EBA;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category10 path{fill:#BA6EBA;}.epoch-theme-dark .epoch.category20b .bar.category10{fill:#BA6EBA;}.epoch-theme-dark .epoch.category20b div.ref.category11{background-color:#9B5C9B;}.epoch-theme-dark .epoch.category20b .category11 .line{stroke:#9B5C9B;}.epoch-theme-dark .epoch.category20b .category11 .area,.epoch-theme-dark .epoch.category20b .category11 .dot{fill:#9B5C9B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category11 path{fill:#9B5C9B;}.epoch-theme-dark .epoch.category20b .bar.category11{fill:#9B5C9B;}.epoch-theme-dark .epoch.category20b div.ref.category12{background-color:#7B487B;}.epoch-theme-dark .epoch.category20b .category12 .line{stroke:#7B487B;}.epoch-theme-dark .epoch.category20b .category12 .area,.epoch-theme-dark .epoch.category20b .category12 .dot{fill:#7B487B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category12 path{fill:#7B487B;}.epoch-theme-dark .epoch.category20b .bar.category12{fill:#7B487B;}.epoch-theme-dark .epoch.category20b div.ref.category13{background-color:#78E8D3;}.epoch-theme-dark .epoch.category20b .category13 .line{stroke:#78E8D3;}.epoch-theme-dark .epoch.category20b .category13 .area,.epoch-theme-dark .epoch.category20b .category13 .dot{fill:#78E8D3;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category13 path{fill:#78E8D3;}.epoch-theme-dark .epoch.category20b .bar.category13{fill:#78E8D3;}.epoch-theme-dark .epoch.category20b div.ref.category14{background-color:#60BAAA;}.epoch-theme-dark .epoch.category20b .category14 .line{stroke:#60BAAA;}.epoch-theme-dark .epoch.category20b .category14 .area,.epoch-theme-dark .epoch.category20b .category14 .dot{fill:#60BAAA;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category14 path{fill:#60BAAA;}.epoch-theme-dark .epoch.category20b .bar.category14{fill:#60BAAA;}.epoch-theme-dark .epoch.category20b div.ref.category15{background-color:#509B8D;}.epoch-theme-dark .epoch.category20b .category15 .line{stroke:#509B8D;}.epoch-theme-dark .epoch.category20b .category15 .area,.epoch-theme-dark .epoch.category20b .category15 .dot{fill:#509B8D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category15 path{fill:#509B8D;}.epoch-theme-dark .epoch.category20b .bar.category15{fill:#509B8D;}.epoch-theme-dark .epoch.category20b div.ref.category16{background-color:#3F7B70;}.epoch-theme-dark .epoch.category20b .category16 .line{stroke:#3F7B70;}.epoch-theme-dark .epoch.category20b .category16 .area,.epoch-theme-dark .epoch.category20b .category16 .dot{fill:#3F7B70;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category16 path{fill:#3F7B70;}.epoch-theme-dark .epoch.category20b .bar.category16{fill:#3F7B70;}.epoch-theme-dark .epoch.category20b div.ref.category17{background-color:#C2FF97;}.epoch-theme-dark .epoch.category20b .category17 .line{stroke:#C2FF97;}.epoch-theme-dark .epoch.category20b .category17 .area,.epoch-theme-dark .epoch.category20b .category17 .dot{fill:#C2FF97;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category17 path{fill:#C2FF97;}.epoch-theme-dark .epoch.category20b .bar.category17{fill:#C2FF97;}.epoch-theme-dark .epoch.category20b div.ref.category18{background-color:#9FD17C;}.epoch-theme-dark .epoch.category20b .category18 .line{stroke:#9FD17C;}.epoch-theme-dark .epoch.category20b .category18 .area,.epoch-theme-dark .epoch.category20b .category18 .dot{fill:#9FD17C;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category18 path{fill:#9FD17C;}.epoch-theme-dark .epoch.category20b .bar.category18{fill:#9FD17C;}.epoch-theme-dark .epoch.category20b div.ref.category19{background-color:#7DA361;}.epoch-theme-dark .epoch.category20b .category19 .line{stroke:#7DA361;}.epoch-theme-dark .epoch.category20b .category19 .area,.epoch-theme-dark .epoch.category20b .category19 .dot{fill:#7DA361;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category19 path{fill:#7DA361;}.epoch-theme-dark .epoch.category20b .bar.category19{fill:#7DA361;}.epoch-theme-dark .epoch.category20b div.ref.category20{background-color:#65854E;}.epoch-theme-dark .epoch.category20b .category20 .line{stroke:#65854E;}.epoch-theme-dark .epoch.category20b .category20 .area,.epoch-theme-dark .epoch.category20b .category20 .dot{fill:#65854E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20b .arc.category20 path{fill:#65854E;}.epoch-theme-dark .epoch.category20b .bar.category20{fill:#65854E;}.epoch-theme-dark .epoch.category20c div.ref.category1{background-color:#B7BCD1;}.epoch-theme-dark .epoch.category20c .category1 .line{stroke:#B7BCD1;}.epoch-theme-dark .epoch.category20c .category1 .area,.epoch-theme-dark .epoch.category20c .category1 .dot{fill:#B7BCD1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category1 path{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20c .bar.category1{fill:#B7BCD1;}.epoch-theme-dark .epoch.category20c div.ref.category2{background-color:#979DAD;}.epoch-theme-dark .epoch.category20c .category2 .line{stroke:#979DAD;}.epoch-theme-dark .epoch.category20c .category2 .area,.epoch-theme-dark .epoch.category20c .category2 .dot{fill:#979DAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category2 path{fill:#979DAD;}.epoch-theme-dark .epoch.category20c .bar.category2{fill:#979DAD;}.epoch-theme-dark .epoch.category20c div.ref.category3{background-color:#6E717D;}.epoch-theme-dark .epoch.category20c .category3 .line{stroke:#6E717D;}.epoch-theme-dark .epoch.category20c .category3 .area,.epoch-theme-dark .epoch.category20c .category3 .dot{fill:#6E717D;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category3 path{fill:#6E717D;}.epoch-theme-dark .epoch.category20c .bar.category3{fill:#6E717D;}.epoch-theme-dark .epoch.category20c div.ref.category4{background-color:#595C66;}.epoch-theme-dark .epoch.category20c .category4 .line{stroke:#595C66;}.epoch-theme-dark .epoch.category20c .category4 .area,.epoch-theme-dark .epoch.category20c .category4 .dot{fill:#595C66;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category4 path{fill:#595C66;}.epoch-theme-dark .epoch.category20c .bar.category4{fill:#595C66;}.epoch-theme-dark .epoch.category20c div.ref.category5{background-color:#FF857F;}.epoch-theme-dark .epoch.category20c .category5 .line{stroke:#FF857F;}.epoch-theme-dark .epoch.category20c .category5 .area,.epoch-theme-dark .epoch.category20c .category5 .dot{fill:#FF857F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category5 path{fill:#FF857F;}.epoch-theme-dark .epoch.category20c .bar.category5{fill:#FF857F;}.epoch-theme-dark .epoch.category20c div.ref.category6{background-color:#DE746E;}.epoch-theme-dark .epoch.category20c .category6 .line{stroke:#DE746E;}.epoch-theme-dark .epoch.category20c .category6 .area,.epoch-theme-dark .epoch.category20c .category6 .dot{fill:#DE746E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category6 path{fill:#DE746E;}.epoch-theme-dark .epoch.category20c .bar.category6{fill:#DE746E;}.epoch-theme-dark .epoch.category20c div.ref.category7{background-color:#B55F5A;}.epoch-theme-dark .epoch.category20c .category7 .line{stroke:#B55F5A;}.epoch-theme-dark .epoch.category20c .category7 .area,.epoch-theme-dark .epoch.category20c .category7 .dot{fill:#B55F5A;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category7 path{fill:#B55F5A;}.epoch-theme-dark .epoch.category20c .bar.category7{fill:#B55F5A;}.epoch-theme-dark .epoch.category20c div.ref.category8{background-color:#964E4B;}.epoch-theme-dark .epoch.category20c .category8 .line{stroke:#964E4B;}.epoch-theme-dark .epoch.category20c .category8 .area,.epoch-theme-dark .epoch.category20c .category8 .dot{fill:#964E4B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category8 path{fill:#964E4B;}.epoch-theme-dark .epoch.category20c .bar.category8{fill:#964E4B;}.epoch-theme-dark .epoch.category20c div.ref.category9{background-color:#F3DE88;}.epoch-theme-dark .epoch.category20c .category9 .line{stroke:#F3DE88;}.epoch-theme-dark .epoch.category20c .category9 .area,.epoch-theme-dark .epoch.category20c .category9 .dot{fill:#F3DE88;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category9 path{fill:#F3DE88;}.epoch-theme-dark .epoch.category20c .bar.category9{fill:#F3DE88;}.epoch-theme-dark .epoch.category20c div.ref.category10{background-color:#DBC87B;}.epoch-theme-dark .epoch.category20c .category10 .line{stroke:#DBC87B;}.epoch-theme-dark .epoch.category20c .category10 .area,.epoch-theme-dark .epoch.category20c .category10 .dot{fill:#DBC87B;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category10 path{fill:#DBC87B;}.epoch-theme-dark .epoch.category20c .bar.category10{fill:#DBC87B;}.epoch-theme-dark .epoch.category20c div.ref.category11{background-color:#BAAA68;}.epoch-theme-dark .epoch.category20c .category11 .line{stroke:#BAAA68;}.epoch-theme-dark .epoch.category20c .category11 .area,.epoch-theme-dark .epoch.category20c .category11 .dot{fill:#BAAA68;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category11 path{fill:#BAAA68;}.epoch-theme-dark .epoch.category20c .bar.category11{fill:#BAAA68;}.epoch-theme-dark .epoch.category20c div.ref.category12{background-color:#918551;}.epoch-theme-dark .epoch.category20c .category12 .line{stroke:#918551;}.epoch-theme-dark .epoch.category20c .category12 .area,.epoch-theme-dark .epoch.category20c .category12 .dot{fill:#918551;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category12 path{fill:#918551;}.epoch-theme-dark .epoch.category20c .bar.category12{fill:#918551;}.epoch-theme-dark .epoch.category20c div.ref.category13{background-color:#C9935E;}.epoch-theme-dark .epoch.category20c .category13 .line{stroke:#C9935E;}.epoch-theme-dark .epoch.category20c .category13 .area,.epoch-theme-dark .epoch.category20c .category13 .dot{fill:#C9935E;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category13 path{fill:#C9935E;}.epoch-theme-dark .epoch.category20c .bar.category13{fill:#C9935E;}.epoch-theme-dark .epoch.category20c div.ref.category14{background-color:#B58455;}.epoch-theme-dark .epoch.category20c .category14 .line{stroke:#B58455;}.epoch-theme-dark .epoch.category20c .category14 .area,.epoch-theme-dark .epoch.category20c .category14 .dot{fill:#B58455;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category14 path{fill:#B58455;}.epoch-theme-dark .epoch.category20c .bar.category14{fill:#B58455;}.epoch-theme-dark .epoch.category20c div.ref.category15{background-color:#997048;}.epoch-theme-dark .epoch.category20c .category15 .line{stroke:#997048;}.epoch-theme-dark .epoch.category20c .category15 .area,.epoch-theme-dark .epoch.category20c .category15 .dot{fill:#997048;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category15 path{fill:#997048;}.epoch-theme-dark .epoch.category20c .bar.category15{fill:#997048;}.epoch-theme-dark .epoch.category20c div.ref.category16{background-color:#735436;}.epoch-theme-dark .epoch.category20c .category16 .line{stroke:#735436;}.epoch-theme-dark .epoch.category20c .category16 .area,.epoch-theme-dark .epoch.category20c .category16 .dot{fill:#735436;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category16 path{fill:#735436;}.epoch-theme-dark .epoch.category20c .bar.category16{fill:#735436;}.epoch-theme-dark .epoch.category20c div.ref.category17{background-color:#A488FF;}.epoch-theme-dark .epoch.category20c .category17 .line{stroke:#A488FF;}.epoch-theme-dark .epoch.category20c .category17 .area,.epoch-theme-dark .epoch.category20c .category17 .dot{fill:#A488FF;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category17 path{fill:#A488FF;}.epoch-theme-dark .epoch.category20c .bar.category17{fill:#A488FF;}.epoch-theme-dark .epoch.category20c div.ref.category18{background-color:#8670D1;}.epoch-theme-dark .epoch.category20c .category18 .line{stroke:#8670D1;}.epoch-theme-dark .epoch.category20c .category18 .area,.epoch-theme-dark .epoch.category20c .category18 .dot{fill:#8670D1;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category18 path{fill:#8670D1;}.epoch-theme-dark .epoch.category20c .bar.category18{fill:#8670D1;}.epoch-theme-dark .epoch.category20c div.ref.category19{background-color:#705CAD;}.epoch-theme-dark .epoch.category20c .category19 .line{stroke:#705CAD;}.epoch-theme-dark .epoch.category20c .category19 .area,.epoch-theme-dark .epoch.category20c .category19 .dot{fill:#705CAD;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category19 path{fill:#705CAD;}.epoch-theme-dark .epoch.category20c .bar.category19{fill:#705CAD;}.epoch-theme-dark .epoch.category20c div.ref.category20{background-color:#52447F;}.epoch-theme-dark .epoch.category20c .category20 .line{stroke:#52447F;}.epoch-theme-dark .epoch.category20c .category20 .area,.epoch-theme-dark .epoch.category20c .category20 .dot{fill:#52447F;stroke:rgba(0, 0, 0, 0);}.epoch-theme-dark .epoch.category20c .arc.category20 path{fill:#52447F;}.epoch-theme-dark .epoch.category20c .bar.category20{fill:#52447F;} \ No newline at end of file diff --git a/examples/realtime-advanced/resources/static/epoch.min.js b/examples/realtime-advanced/resources/static/epoch.min.js new file mode 100644 index 00000000..0c654b86 --- /dev/null +++ b/examples/realtime-advanced/resources/static/epoch.min.js @@ -0,0 +1,114 @@ +(function(){var e;null==window.Epoch&&(window.Epoch={});null==(e=window.Epoch).Chart&&(e.Chart={});null==(e=window.Epoch).Time&&(e.Time={});null==(e=window.Epoch).Util&&(e.Util={});null==(e=window.Epoch).Formats&&(e.Formats={});Epoch.warn=function(g){return(console.warn||console.log)("Epoch Warning: "+g)};Epoch.exception=function(g){throw"Epoch Error: "+g;}}).call(this); +(function(){Epoch.TestContext=function(){function e(){var c,a,d;this._log=[];a=0;for(d=g.length;ac){if((c|0)!==c||d)c=c.toFixed(a);return c}f="KMGTPEZY".split("");for(h in f)if(k=f[h],b=Math.pow(10,3*((h|0)+1)),c>=b&&cc){if(0!==c%1||d)c=c.toFixed(a);return""+c+" B"}f="KB MB GB TB PB EB ZB YB".split(" ");for(h in f)if(k=f[h],b=Math.pow(1024,(h|0)+1),c>=b&&cf;k=1<=f?++a:--a)q.push(arguments[k]);return q}.apply(this,arguments);c=this._events[a];m=[];f=0;for(q=c.length;fthis.options.windowSize+1&&a.values.shift();b=[this._ticks[0],this._ticks[this._ticks.length-1]];a=b[0];b=b[1];null!=b&&b.enter&&(b.enter=!1,b.opacity=1);null!=a&&a.exit&&this._shiftTick();this.animation.frame=0;this.trigger("transition:end");if(0this.options.queueSize&&this._queue.splice(this.options.queueSize,this._queue.length-this.options.queueSize);if(this._queue.length===this.options.queueSize)return!1;this._queue.push(a.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));this.trigger("push");if(!this.inTransition())return this._startTransition()}; +a.prototype._shift=function(){var a,b,c,d;this.trigger("before:shift");a=this._queue.shift();d=this.data;for(b in d)c=d[b],c.values.push(a[b]);this._updateTicks(a[0].time);this._transitionRangeAxes();return this.trigger("after:shift")};a.prototype._transitionRangeAxes=function(){this.hasAxis("left")&&this.svg.selectAll(".y.axis.left").transition().duration(500).ease("linear").call(this.leftAxis());if(this.hasAxis("right"))return this.svg.selectAll(".y.axis.right").transition().duration(500).ease("linear").call(this.rightAxis())}; +a.prototype._animate=function(){if(this.inTransition())return++this.animation.frame===this.animation.duration&&this._stopTransition(),this.draw(this.animation.frame*this.animation.delta()),this._updateTimeAxes()};a.prototype.y=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.extent(function(a){return a.y})).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.w=function(){return this.innerWidth()/ +this.options.windowSize};a.prototype._updateTicks=function(a){if(this.hasAxis("top")||this.hasAxis("bottom"))if(++this._tickTimer%this.options.ticks.time||this._pushTick(this.options.windowSize,a,!0),!(0<=this._ticks[0].x-this.w()/this.pixelRatio))return this._ticks[0].exit=!0};a.prototype._pushTick=function(a,b,c,d){null==c&&(c=!1);null==d&&(d=!1);if(this.hasAxis("top")||this.hasAxis("bottom"))return b={time:b,x:a*(this.w()/this.pixelRatio)+this._offsetX(),opacity:c?0:1,enter:c?!0:!1,exit:!1},this.hasAxis("bottom")&& +(a=this.bottomAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",6),a.append("text").attr("text-anchor","middle").attr("dy",19).text(this.options.tickFormats.bottom(b.time)),b.bottomEl=a),this.hasAxis("top")&&(a=this.topAxis.append("g").attr("class","tick major").attr("transform","translate("+(b.x+1)+",0)").style("opacity",b.opacity),a.append("line").attr("y2",-6),a.append("text").attr("text-anchor","middle").attr("dy", +-10).text(this.options.tickFormats.top(b.time)),b.topEl=a),d?this._ticks.unshift(b):this._ticks.push(b),b};a.prototype._shiftTick=function(){var a;if(0f;b=0<=f?++c:--c)k=0,e.push(function(){var a,c,d,f;d=this.data;f=[];a=0;for(c=d.length;ag;a=0<=g?++f:--f){b=e=k=0;for(m=this.data.length;0<=m?em;b=0<=m?++e:--e)k+=this.data[b].values[a].y;k>c&&(c=k)}return[0,c]};return a}(Epoch.Time.Plot)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Area=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype.setStyles=function(a){a=null!=a.className?this.getStyles("g."+a.className.replace(/\s/g,".")+" path.area"):this.getStyles("g path.area");this.ctx.fillStyle=a.fill;null!=a.stroke&&(this.ctx.strokeStyle= +a.stroke);if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype._drawAreas=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);g=[this.y(),this.w()];m=g[0];g=g[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize,f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize- +1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);c=e?(c+3)*g+a:(c+2)*g+a;this.ctx.lineTo(c,this.innerHeight());this.ctx.lineTo(this.width*this.pixelRatio+g+a,this.innerHeight());this.ctx.closePath();p.push(this.ctx.fill())}return p};a.prototype._drawStrokes=function(a){var b,c,k,f,e,g,m,l,n,p;null==a&&(a=0);c=[this.y(),this.w()];m=c[0];g=c[1];p=[];for(c=l=n=this.data.length-1;0>=n?0>=l:0<=l;c=0>=n?++l:--l){f=this.data[c];this.setStyles(f);this.ctx.beginPath();e=[this.options.windowSize, +f.values.length,this.inTransition()];c=e[0];k=e[1];for(e=e[2];-2<=--c&&0<=--k;)b=f.values[k],b=[(c+1)*g+a,m(b.y+b.y0)],e&&(b[0]+=g),c===this.options.windowSize-1?this.ctx.moveTo.apply(this.ctx,b):this.ctx.lineTo.apply(this.ctx,b);p.push(this.ctx.stroke())}return p};a.prototype.draw=function(c){null==c&&(c=0);this.clear();this._drawAreas(c);this._drawStrokes(c);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Bar=function(c){function a(){return a.__super__.constructor.apply(this,arguments)}g(a,c);a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype.setStyles=function(a){a=this.getStyles("rect.bar."+a.replace(/\s/g,"."));this.ctx.fillStyle=a.fill;this.ctx.strokeStyle= +null==a.stroke||"none"===a.stroke?"transparent":a.stroke;if(null!=a["stroke-width"])return this.ctx.lineWidth=a["stroke-width"].replace("px","")};a.prototype.draw=function(c){var b,h,k,f,e,g,m,l,n,p,r,s,t;null==c&&(c=0);this.clear();f=[this.y(),this.w()];p=f[0];n=f[1];t=this.data;r=0;for(s=t.length;r=e&&0<=--g;)b=m.values[g],k=[f*n+c, +b.y,b.y0],b=k[0],h=k[1],k=k[2],l&&(b+=n),b=[b+1,p(h+k),n-2,this.innerHeight()-p(h)+0.5*this.pixelRatio],this.ctx.fillRect.apply(this.ctx,b),this.ctx.strokeRect.apply(this.ctx,b);return a.__super__.draw.call(this)};return a}(Epoch.Time.Stack)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Gauge=function(c){function a(c){this.options=null!=c?c:{};a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,d));this.value=this.options.value||0;"absolute"!==this.el.style("position")&&"relative"!==this.el.style("position")&&this.el.style("position","relative"); +this.svg=this.el.insert("svg",":first-child").attr("width",this.width).attr("height",this.height).attr("class","gauge-labels");this.svg.style({position:"absolute","z-index":"1"});this.svg.append("g").attr("transform","translate("+this.textX()+", "+this.textY()+")").append("text").attr("class","value").text(this.options.format(this.value));this.animation={interval:null,active:!1,delta:0,target:0};this._animate=function(a){return function(){Math.abs(a.animation.target-a.value)=t;b=0<=t?++s:--s)b=l(b),b=[Math.cos(b),Math.sin(b)],c=b[0],m=b[1],b=c*(g-n)+d,r=m*(g-n)+e,c=c*(g-n-p)+d,m=m*(g-n-p)+e,this.ctx.moveTo(b,r),this.ctx.lineTo(c,m);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.outer");this.ctx.beginPath();this.ctx.arc(d,e,g,-1.125* +Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.setStyles(".epoch .gauge .arc.inner");this.ctx.beginPath();this.ctx.arc(d,e,g-10,-1.125*Math.PI,0.125*Math.PI,!1);this.ctx.stroke();this.drawNeedle();return a.__super__.draw.call(this)};a.prototype.drawNeedle=function(){var a,b,c;c=[this.centerX(),this.centerY(),this.radius()];a=c[0];b=c[1];c=c[2];this.setStyles(".epoch .gauge .needle");this.ctx.beginPath();this.ctx.save();this.ctx.translate(a,b);this.ctx.rotate(this.getAngle(this.value));this.ctx.moveTo(4* +this.pixelRatio,0);this.ctx.lineTo(-4*this.pixelRatio,0);this.ctx.lineTo(-1*this.pixelRatio,19-c);this.ctx.lineTo(1,19-c);this.ctx.fill();this.setStyles(".epoch .gauge .needle-base");this.ctx.beginPath();this.ctx.arc(0,0,this.getWidth()/25,0,2*Math.PI);this.ctx.fill();return this.ctx.restore()};a.prototype.domainChanged=function(){return this.draw()};a.prototype.ticksChanged=function(){return this.draw()};a.prototype.tickSizeChanged=function(){return this.draw()};a.prototype.tickOffsetChanged=function(){return this.draw()}; +a.prototype.formatChanged=function(){return this.svg.select("text.value").text(this.options.format(this.value))};return a}(Epoch.Chart.Canvas)}).call(this); +(function(){var e={}.hasOwnProperty,g=function(c,a){function d(){this.constructor=c}for(var b in a)e.call(a,b)&&(c[b]=a[b]);d.prototype=a.prototype;c.prototype=new d;c.__super__=a.prototype;return c};Epoch.Time.Heatmap=function(c){function a(c){this.options=c;a.__super__.constructor.call(this,this.options=Epoch.Util.defaults(this.options,b));this._setOpacityFunction();this._setupPaintCanvas();this.onAll(e)}var d,b,e;g(a,c);b={buckets:10,bucketRange:[0,100],opacity:"linear",bucketPadding:2,paintZeroValues:!1, +cutOutliers:!1};d={root:function(a,b){return Math.pow(a/b,0.5)},linear:function(a,b){return a/b},quadratic:function(a,b){return Math.pow(a/b,2)},cubic:function(a,b){return Math.pow(a/b,3)},quartic:function(a,b){return Math.pow(a/b,4)},quintic:function(a,b){return Math.pow(a/b,5)}};e={"option:buckets":"bucketsChanged","option:bucketRange":"bucketRangeChanged","option:opacity":"opacityChanged","option:bucketPadding":"bucketPaddingChanged","option:paintZeroValues":"paintZeroValuesChanged","option:cutOutliers":"cutOutliersChanged"}; +a.prototype._setOpacityFunction=function(){if(Epoch.isString(this.options.opacity)){if(this._opacityFn=d[this.options.opacity],null==this._opacityFn)return Epoch.exception("Unknown coloring function provided '"+this.options.opacity+"'")}else return Epoch.isFunction(this.options.opacity)?this._opacityFn=this.options.opacity:Epoch.exception("Unknown type for provided coloring function.")};a.prototype.setData=function(b){var c,d,e,g;a.__super__.setData.call(this,b);e=this.data;g=[];c=0;for(d=e.length;c< +d;c++)b=e[c],g.push(b.values=b.values.map(function(a){return function(b){return a._prepareEntry(b)}}(this)));return g};a.prototype._getBuckets=function(a){var b,c,d,e,g;e=a.time;g=[];b=0;for(d=this.options.buckets;0<=d?bd;0<=d?++b:--b)g.push(0);e={time:e,max:0,buckets:g};b=(this.options.bucketRange[1]-this.options.bucketRange[0])/this.options.buckets;g=a.histogram;for(c in g)a=g[c],d=parseInt((c-this.options.bucketRange[0])/b),this.options.cutOutliers&&(0>d||d>=this.options.buckets)||(0>d?d= +0:d>=this.options.buckets&&(d=this.options.buckets-1),e.buckets[d]+=parseInt(a));c=a=0;for(b=e.buckets.length;0<=b?ab;c=0<=b?++a:--a)e.max=Math.max(e.max,e.buckets[c]);return e};a.prototype.y=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight(),0])};a.prototype.ySvg=function(){return d3.scale.linear().domain(this.options.bucketRange).range([this.innerHeight()/this.pixelRatio,0])};a.prototype.h=function(){return this.innerHeight()/this.options.buckets}; +a.prototype._offsetX=function(){return 0.5*this.w()/this.pixelRatio};a.prototype._setupPaintCanvas=function(){this.paintWidth=(this.options.windowSize+1)*this.w();this.paintHeight=this.height*this.pixelRatio;this.paint=document.createElement("CANVAS");this.paint.width=this.paintWidth;this.paint.height=this.paintHeight;this.p=Epoch.Util.getContext(this.paint);this.redraw();this.on("after:shift","_paintEntry");this.on("transition:end","_shiftPaintCanvas");return this.on("transition:end",function(a){return function(){return a.draw(a.animation.frame* +a.animation.delta())}}(this))};a.prototype.redraw=function(){var a,b;b=this.data[0].values.length;a=this.options.windowSize;for(this.inTransition()&&a++;0<=--b&&0<=--a;)this._paintEntry(b,a);return this.draw(this.animation.frame*this.animation.delta())};a.prototype._computeColor=function(a,b,c){return Epoch.Util.toRGBA(c,this._opacityFn(a,b))};a.prototype._paintEntry=function(a,b){var c,d,e,g,h,p,r,s,t,v,y,w,A,z;null==a&&(a=null);null==b&&(b=null);g=[this.w(),this.h()];y=g[0];p=g[1];null==a&&(a=this.data[0].values.length- +1);null==b&&(b=this.options.windowSize);g=[];var x;x=[];h=0;for(v=this.options.buckets;0<=v?hv;0<=v?++h:--h)x.push(0);v=0;t=this.data;d=0;for(r=t.length;d code[class*="language-"], +pre[class*="language-"] { + background: #f5f2f0; +} + +/* Inline code */ +:not(pre) > code[class*="language-"] { + padding: .1em; + border-radius: .3em; +} + +.token.comment, +.token.prolog, +.token.doctype, +.token.cdata { + color: slategray; +} + +.token.punctuation { + color: #999; +} + +.namespace { + opacity: .7; +} + +.token.property, +.token.tag, +.token.boolean, +.token.number, +.token.constant, +.token.symbol, +.token.deleted { + color: #905; +} + +.token.selector, +.token.attr-name, +.token.string, +.token.char, +.token.builtin, +.token.inserted { + color: #690; +} + +.token.operator, +.token.entity, +.token.url, +.language-css .token.string, +.style .token.string { + color: #a67f59; + background: hsla(0, 0%, 100%, .5); +} + +.token.atrule, +.token.attr-value, +.token.keyword { + color: #07a; +} + +.token.function { + color: #DD4A68; +} + +.token.regex, +.token.important, +.token.variable { + color: #e90; +} + +.token.important, +.token.bold { + font-weight: bold; +} +.token.italic { + font-style: italic; +} + +.token.entity { + cursor: help; +} + diff --git a/examples/realtime-advanced/resources/static/prismjs.min.js b/examples/realtime-advanced/resources/static/prismjs.min.js new file mode 100644 index 00000000..a6855a78 --- /dev/null +++ b/examples/realtime-advanced/resources/static/prismjs.min.js @@ -0,0 +1,5 @@ +/* http://prismjs.com/download.html?themes=prism&languages=clike+javascript+go */ +self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{};var Prism=function(){var e=/\blang(?:uage)?-(?!\*)(\w+)\b/i,t=self.Prism={util:{encode:function(e){return e instanceof n?new n(e.type,t.util.encode(e.content),e.alias):"Array"===t.util.type(e)?e.map(t.util.encode):e.replace(/&/g,"&").replace(/e.length)break e;if(!(d instanceof a)){u.lastIndex=0;var m=u.exec(d);if(m){c&&(f=m[1].length);var y=m.index-1+f,m=m[0].slice(f),v=m.length,k=y+v,b=d.slice(0,y+1),w=d.slice(k+1),N=[p,1];b&&N.push(b);var O=new a(l,g?t.tokenize(m,g):m,h);N.push(O),w&&N.push(w),Array.prototype.splice.apply(r,N)}}}}}return r},hooks:{all:{},add:function(e,n){var a=t.hooks.all;a[e]=a[e]||[],a[e].push(n)},run:function(e,n){var a=t.hooks.all[e];if(a&&a.length)for(var r,i=0;r=a[i++];)r(n)}}},n=t.Token=function(e,t,n){this.type=e,this.content=t,this.alias=n};if(n.stringify=function(e,a,r){if("string"==typeof e)return e;if("Array"===t.util.type(e))return e.map(function(t){return n.stringify(t,a,e)}).join("");var i={type:e.type,content:n.stringify(e.content,a,r),tag:"span",classes:["token",e.type],attributes:{},language:a,parent:r};if("comment"==i.type&&(i.attributes.spellcheck="true"),e.alias){var l="Array"===t.util.type(e.alias)?e.alias:[e.alias];Array.prototype.push.apply(i.classes,l)}t.hooks.run("wrap",i);var s="";for(var o in i.attributes)s+=o+'="'+(i.attributes[o]||"")+'"';return"<"+i.tag+' class="'+i.classes.join(" ")+'" '+s+">"+i.content+""},!self.document)return self.addEventListener?(self.addEventListener("message",function(e){var n=JSON.parse(e.data),a=n.language,r=n.code;self.postMessage(JSON.stringify(t.util.encode(t.tokenize(r,t.languages[a])))),self.close()},!1),self.Prism):self.Prism;var a=document.getElementsByTagName("script");return a=a[a.length-1],a&&(t.filename=a.src,document.addEventListener&&!a.hasAttribute("data-manual")&&document.addEventListener("DOMContentLoaded",t.highlightAll)),self.Prism}();"undefined"!=typeof module&&module.exports&&(module.exports=Prism);; +Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\w\W]*?\*\//,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0}],string:/("|')(\\\n|\\?.)*?\1/,"class-name":{pattern:/((?:(?:class|interface|extends|implements|trait|instanceof|new)\s+)|(?:catch\s+\())[a-z0-9_\.\\]+/i,lookbehind:!0,inside:{punctuation:/(\.|\\)/}},keyword:/\b(if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,"boolean":/\b(true|false)\b/,"function":{pattern:/[a-z0-9_]+\(/i,inside:{punctuation:/\(/}},number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee]-?\d+)?)\b/,operator:/[-+]{1,2}|!|<=?|>=?|={1,3}|&{1,2}|\|?\||\?|\*|\/|~|\^|%/,ignore:/&(lt|gt|amp);/i,punctuation:/[{}[\];(),.:]/};; +Prism.languages.javascript=Prism.languages.extend("clike",{keyword:/\b(break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|false|finally|for|function|get|if|implements|import|in|instanceof|interface|let|new|null|package|private|protected|public|return|set|static|super|switch|this|throw|true|try|typeof|var|void|while|with|yield)\b/,number:/\b-?(0x[\dA-Fa-f]+|\d*\.?\d+([Ee][+-]?\d+)?|NaN|-?Infinity)\b/,"function":/(?!\d)[a-z0-9_$]+(?=\()/i}),Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/(^|[^/])\/(?!\/)(\[.+?]|\\.|[^/\r\n])+\/[gim]{0,3}(?=\s*($|[\r\n,.;})]))/,lookbehind:!0}}),Prism.languages.markup&&Prism.languages.insertBefore("markup","tag",{script:{pattern:/[\w\W]*?<\/script>/i,inside:{tag:{pattern:/|<\/script>/i,inside:Prism.languages.markup.tag.inside},rest:Prism.languages.javascript},alias:"language-javascript"}});; +Prism.languages.go=Prism.languages.extend("clike",{keyword:/\b(break|case|chan|const|continue|default|defer|else|fallthrough|for|func|go(to)?|if|import|interface|map|package|range|return|select|struct|switch|type|var)\b/,builtin:/\b(bool|byte|complex(64|128)|error|float(32|64)|rune|string|u?int(8|16|32|64|)|uintptr|append|cap|close|complex|copy|delete|imag|len|make|new|panic|print(ln)?|real|recover)\b/,"boolean":/\b(_|iota|nil|true|false)\b/,operator:/([(){}\[\]]|[*\/%^!]=?|\+[=+]?|-[>=-]?|\|[=|]?|>[=>]?|<(<|[=-])?|==?|&(&|=|^=?)?|\.(\.\.)?|[,;]|:=?)/,number:/\b(-?(0x[a-f\d]+|(\d+\.?\d*|\.\d+)(e[-+]?\d+)?)i?)\b/i,string:/("|'|`)(\\?.|\r|\n)*?\1/}),delete Prism.languages.go["class-name"];; diff --git a/examples/realtime-advanced/resources/static/realtime.js b/examples/realtime-advanced/resources/static/realtime.js new file mode 100644 index 00000000..919dae26 --- /dev/null +++ b/examples/realtime-advanced/resources/static/realtime.js @@ -0,0 +1,144 @@ + + +function StartRealtime(roomid, timestamp) { + StartEpoch(timestamp); + StartSSE(roomid); + StartForm(); +} + +function StartForm() { + $('#chat-message').focus(); + $('#chat-form').ajaxForm(function() { + $('#chat-message').val(''); + $('#chat-message').focus(); + }); +} + +function StartEpoch(timestamp) { + var windowSize = 60; + var height = 200; + var defaultData = histogram(windowSize, timestamp); + + window.heapChart = $('#heapChart').epoch({ + type: 'time.area', + axes: ['bottom', 'left'], + height: height, + historySize: 10, + data: [ + {values: defaultData}, + {values: defaultData} + ] + }); + + window.mallocsChart = $('#mallocsChart').epoch({ + type: 'time.area', + axes: ['bottom', 'left'], + height: height, + historySize: 10, + data: [ + {values: defaultData}, + {values: defaultData} + ] + }); + + window.messagesChart = $('#messagesChart').epoch({ + type: 'time.line', + axes: ['bottom', 'left'], + height: 240, + historySize: 10, + data: [ + {values: defaultData}, + {values: defaultData}, + {values: defaultData} + ] + }); +} + +function StartSSE(roomid) { + if (!window.EventSource) { + alert("EventSource is not enabled in this browser"); + return; + } + var source = new EventSource('/stream/'+roomid); + source.addEventListener('message', newChatMessage, false); + source.addEventListener('stats', stats, false); +} + +function stats(e) { + var data = parseJSONStats(e.data); + heapChart.push(data.heap); + mallocsChart.push(data.mallocs); + messagesChart.push(data.messages); +} + +function parseJSONStats(e) { + var data = jQuery.parseJSON(e); + var timestamp = data.timestamp; + + var heap = [ + {time: timestamp, y: data.HeapInuse}, + {time: timestamp, y: data.StackInuse} + ]; + + var mallocs = [ + {time: timestamp, y: data.Mallocs}, + {time: timestamp, y: data.Frees} + ]; + var messages = [ + {time: timestamp, y: data.Connected}, + {time: timestamp, y: data.Inbound}, + {time: timestamp, y: data.Outbound} + ]; + + return { + heap: heap, + mallocs: mallocs, + messages: messages + } +} + +function newChatMessage(e) { + var data = jQuery.parseJSON(e.data); + var nick = data.nick; + var message = data.message; + var style = rowStyle(nick); + var html = ""+nick+""+message+""; + $('#chat').append(html); + + $("#chat-scroll").scrollTop($("#chat-scroll")[0].scrollHeight); +} + +function histogram(windowSize, timestamp) { + var entries = new Array(windowSize); + for(var i = 0; i < windowSize; i++) { + entries[i] = {time: (timestamp-windowSize+i-1), y:0}; + } + return entries; +} + +var entityMap = { + "&": "&", + "<": "<", + ">": ">", + '"': '"', + "'": ''', + "/": '/' +}; + +function rowStyle(nick) { + var classes = ['active', 'success', 'info', 'warning', 'danger']; + var index = hashCode(nick)%5; + return classes[index]; +} + +function hashCode(s){ + return Math.abs(s.split("").reduce(function(a,b){a=((a<<5)-a)+b.charCodeAt(0);return a&a},0)); +} + +function escapeHtml(string) { + return String(string).replace(/[&<>"'\/]/g, function (s) { + return entityMap[s]; + }); +} + +window.StartRealtime = StartRealtime diff --git a/examples/realtime-advanced/rooms.go b/examples/realtime-advanced/rooms.go new file mode 100644 index 00000000..82396ba3 --- /dev/null +++ b/examples/realtime-advanced/rooms.go @@ -0,0 +1,25 @@ +package main + +import "github.com/dustin/go-broadcast" + +var roomChannels = make(map[string]broadcast.Broadcaster) + +func openListener(roomid string) chan interface{} { + listener := make(chan interface{}) + room(roomid).Register(listener) + return listener +} + +func closeListener(roomid string, listener chan interface{}) { + room(roomid).Unregister(listener) + close(listener) +} + +func room(roomid string) broadcast.Broadcaster { + b, ok := roomChannels[roomid] + if !ok { + b = broadcast.NewBroadcaster(10) + roomChannels[roomid] = b + } + return b +} diff --git a/examples/realtime-advanced/routes.go b/examples/realtime-advanced/routes.go new file mode 100644 index 00000000..b1877565 --- /dev/null +++ b/examples/realtime-advanced/routes.go @@ -0,0 +1,96 @@ +package main + +import ( + "fmt" + "html" + "io" + "strings" + "time" + + "github.com/gin-gonic/gin" +) + +func rateLimit(c *gin.Context) { + + ip := c.ClientIP() + value := int(ips.Add(ip, 1)) + if value%50 == 0 { + fmt.Printf("ip: %s, count: %d\n", ip, value) + } + if value >= 200 { + if value%200 == 0 { + fmt.Println("ip blocked") + } + c.Abort() + c.String(503, "you were automatically banned :)") + } +} + +func index(c *gin.Context) { + c.Redirect(301, "/room/hn") +} + +func roomGET(c *gin.Context) { + roomid := c.Param("roomid") + nick := c.Query("nick") + if len(nick) < 2 { + nick = "" + } + if len(nick) > 13 { + nick = nick[0:12] + "..." + } + c.HTML(200, "room_login.templ.html", gin.H{ + "roomid": roomid, + "nick": nick, + "timestamp": time.Now().Unix(), + }) + +} + +func roomPOST(c *gin.Context) { + roomid := c.Param("roomid") + nick := c.Query("nick") + message := c.PostForm("message") + message = strings.TrimSpace(message) + + validMessage := len(message) > 1 && len(message) < 200 + validNick := len(nick) > 1 && len(nick) < 14 + if !validMessage || !validNick { + c.JSON(400, gin.H{ + "status": "failed", + "error": "the message or nickname is too long", + }) + return + } + + post := gin.H{ + "nick": html.EscapeString(nick), + "message": html.EscapeString(message), + } + messages.Add("inbound", 1) + room(roomid).Submit(post) + c.JSON(200, post) +} + +func streamRoom(c *gin.Context) { + roomid := c.Param("roomid") + listener := openListener(roomid) + ticker := time.NewTicker(1 * time.Second) + users.Add("connected", 1) + defer func() { + closeListener(roomid, listener) + ticker.Stop() + users.Add("disconnected", 1) + }() + + c.Stream(func(w io.Writer) bool { + select { + case msg := <-listener: + messages.Add("outbound", 1) + c.SSEvent("message", msg) + case <-ticker.C: + c.SSEvent("stats", Stats()) + } + return true + }) +} diff --git a/examples/realtime-advanced/stats.go b/examples/realtime-advanced/stats.go new file mode 100644 index 00000000..c36ecc78 --- /dev/null +++ b/examples/realtime-advanced/stats.go @@ -0,0 +1,56 @@ +package main + +import ( + "runtime" + "sync" + "time" + + "github.com/manucorporat/stats" +) + +var ips = stats.New() +var messages = stats.New() +var users = stats.New() +var mutexStats sync.RWMutex +var savedStats map[string]uint64 + +func statsWorker() { + c := time.Tick(1 * time.Second) + var lastMallocs uint64 = 0 + var lastFrees uint64 = 0 + for _ = range c { + var stats runtime.MemStats + runtime.ReadMemStats(&stats) + + mutexStats.Lock() + savedStats = map[string]uint64{ + "timestamp": uint64(time.Now().Unix()), + "HeapInuse": stats.HeapInuse, + "StackInuse": stats.StackInuse, + "Mallocs": (stats.Mallocs - lastMallocs), + "Frees": (stats.Frees - lastFrees), + "Inbound": uint64(messages.Get("inbound")), + "Outbound": uint64(messages.Get("outbound")), + "Connected": connectedUsers(), + } + lastMallocs = stats.Mallocs + lastFrees = stats.Frees + messages.Reset() + mutexStats.Unlock() + } +} + +func connectedUsers() uint64 { + connected := users.Get("connected") - users.Get("disconnected") + if connected < 0 { + return 0 + } + return uint64(connected) +} + +func Stats() map[string]uint64 { + mutexStats.RLock() + defer mutexStats.RUnlock() + + return savedStats +} diff --git a/examples/realtime-chat/main.go b/examples/realtime-chat/main.go new file mode 100644 index 00000000..e4b55a0f --- /dev/null +++ b/examples/realtime-chat/main.go @@ -0,0 +1,58 @@ +package main + +import ( + "fmt" + "io" + "math/rand" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.SetHTMLTemplate(html) + + router.GET("/room/:roomid", roomGET) + router.POST("/room/:roomid", roomPOST) + router.DELETE("/room/:roomid", roomDELETE) + router.GET("/stream/:roomid", stream) + + router.Run(":8080") +} + +func stream(c *gin.Context) { + roomid := c.Param("roomid") + listener := openListener(roomid) + defer closeListener(roomid, listener) + + c.Stream(func(w io.Writer) bool { + c.SSEvent("message", <-listener) + return true + }) +} + +func roomGET(c *gin.Context) { + roomid := c.Param("roomid") + userid := fmt.Sprint(rand.Int31()) + c.HTML(200, "chat_room", gin.H{ + "roomid": roomid, + "userid": userid, + }) +} + +func roomPOST(c *gin.Context) { + roomid := c.Param("roomid") + userid := c.PostForm("user") + message := c.PostForm("message") + room(roomid).Submit(userid + ": " + message) + + c.JSON(200, gin.H{ + "status": "success", + "message": message, + }) +} + +func roomDELETE(c *gin.Context) { + roomid := c.Param("roomid") + deleteBroadcast(roomid) +} diff --git a/examples/realtime-chat/rooms.go b/examples/realtime-chat/rooms.go new file mode 100644 index 00000000..8c62bece --- /dev/null +++ b/examples/realtime-chat/rooms.go @@ -0,0 +1,33 @@ +package main + +import "github.com/dustin/go-broadcast" + +var roomChannels = make(map[string]broadcast.Broadcaster) + +func openListener(roomid string) chan interface{} { + listener := make(chan interface{}) + room(roomid).Register(listener) + return listener +} + +func closeListener(roomid string, listener chan interface{}) { + room(roomid).Unregister(listener) + close(listener) +} + +func deleteBroadcast(roomid string) { + b, ok := roomChannels[roomid] + if ok { + b.Close() + delete(roomChannels, roomid) + } +} + +func room(roomid string) broadcast.Broadcaster { + b, ok := roomChannels[roomid] + if !ok { + b = broadcast.NewBroadcaster(10) + roomChannels[roomid] = b + } + return b +} diff --git a/examples/realtime-chat/template.go b/examples/realtime-chat/template.go new file mode 100644 index 00000000..b9024de6 --- /dev/null +++ b/examples/realtime-chat/template.go @@ -0,0 +1,44 @@ +package main + +import "html/template" + +var html = template.Must(template.New("chat_room").Parse(` + + + {{.roomid}} + + + + + + +

Welcome to {{.roomid}} room

+
+
+ User: + Message: + +
+ + +`)) diff --git a/fs.go b/fs.go new file mode 100644 index 00000000..f95dc84a --- /dev/null +++ b/fs.go @@ -0,0 +1,43 @@ +package gin + +import ( + "net/http" + "os" +) + +type ( + onlyfilesFS struct { + fs http.FileSystem + } + neuteredReaddirFile struct { + http.File + } +) + +// It returns a http.Filesystem that can be used by http.FileServer(). It is used interally +// in router.Static(). +// if listDirectory == true, then it works the same as http.Dir() otherwise it returns +// a filesystem that prevents http.FileServer() to list the directory files. +func Dir(root string, listDirectory bool) http.FileSystem { + fs := http.Dir(root) + if listDirectory { + return fs + } else { + return &onlyfilesFS{fs} + } +} + +// Conforms to http.Filesystem +func (fs onlyfilesFS) Open(name string) (http.File, error) { + f, err := fs.fs.Open(name) + if err != nil { + return nil, err + } + return neuteredReaddirFile{f}, nil +} + +// Overrides the http.File default implementation +func (f neuteredReaddirFile) Readdir(count int) ([]os.FileInfo, error) { + // this disables directory listing + return nil, nil +} diff --git a/gin.go b/gin.go index ea9345aa..693c7875 100644 --- a/gin.go +++ b/gin.go @@ -5,55 +5,83 @@ package gin import ( - "github.com/gin-gonic/gin/render" - "github.com/julienschmidt/httprouter" "html/template" - "math" + "net" "net/http" + "os" "sync" + + "github.com/gin-gonic/gin/render" ) -const ( - AbortIndex = math.MaxInt8 / 2 - MIMEJSON = "application/json" - MIMEHTML = "text/html" - MIMEXML = "application/xml" - MIMEXML2 = "text/xml" - MIMEPlain = "text/plain" - MIMEPOSTForm = "application/x-www-form-urlencoded" -) +const Version = "v1.0rc2" + +var default404Body = []byte("404 page not found") +var default405Body = []byte("405 method not allowed") type ( - HandlerFunc func(*Context) + HandlerFunc func(*Context) + HandlersChain []HandlerFunc - // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middlewares. + // Represents the web framework, it wraps the blazing fast httprouter multiplexer and a list of global middleware. Engine struct { - *RouterGroup - HTMLRender render.Render - Default404Body []byte - pool sync.Pool - allNoRoute []HandlerFunc - noRoute []HandlerFunc - router *httprouter.Router + RouterGroup + HTMLRender render.HTMLRender + allNoRoute HandlersChain + allNoMethod HandlersChain + noRoute HandlersChain + noMethod HandlersChain + pool sync.Pool + trees methodTrees + + // Enables automatic redirection if the current route can't be matched but a + // handler for the path with (without) the trailing slash exists. + // For example if /foo/ is requested but a route only exists for /foo, the + // client is redirected to /foo with http status code 301 for GET requests + // and 307 for all other request methods. + RedirectTrailingSlash bool + + // If enabled, the router tries to fix the current request path, if no + // handle is registered for it. + // First superfluous path elements like ../ or // are removed. + // Afterwards the router does a case-insensitive lookup of the cleaned path. + // If a handle can be found for this route, the router makes a redirection + // to the corrected path with status code 301 for GET requests and 307 for + // all other request methods. + // For example /FOO and /..//Foo could be redirected to /foo. + // RedirectTrailingSlash is independent of this option. + RedirectFixedPath bool + + // If enabled, the router checks if another method is allowed for the + // current route, if the current request can not be routed. + // If this is the case, the request is answered with 'Method Not Allowed' + // and HTTP status code 405. + // If no other Method is allowed, the request is delegated to the NotFound + // handler. + HandleMethodNotAllowed bool + ForwardedByClientIP bool } ) // Returns a new blank Engine instance without any middleware attached. // The most basic configuration func New() *Engine { - engine := &Engine{} - engine.RouterGroup = &RouterGroup{ - Handlers: nil, - absolutePath: "/", - engine: engine, + debugPrintWARNING() + engine := &Engine{ + RouterGroup: RouterGroup{ + Handlers: nil, + BasePath: "/", + root: true, + }, + RedirectTrailingSlash: true, + RedirectFixedPath: false, + HandleMethodNotAllowed: false, + ForwardedByClientIP: true, + trees: make(methodTrees, 0, 9), } - engine.router = httprouter.New() - engine.Default404Body = []byte("404 page not found") - engine.router.NotFound = engine.handle404 + engine.RouterGroup.engine = engine engine.pool.New = func() interface{} { - c := &Context{Engine: engine} - c.Writer = &c.writermem - return c + return engine.allocateContext() } return engine } @@ -65,10 +93,13 @@ func Default() *Engine { return engine } +func (engine *Engine) allocateContext() *Context { + return &Context{engine: engine} +} + func (engine *Engine) LoadHTMLGlob(pattern string) { if IsDebugging() { - render.HTMLDebug.AddGlob(pattern) - engine.HTMLRender = render.HTMLDebug + engine.HTMLRender = render.HTMLDebug{Glob: pattern} } else { templ := template.Must(template.ParseGlob(pattern)) engine.SetHTMLTemplate(templ) @@ -77,8 +108,7 @@ func (engine *Engine) LoadHTMLGlob(pattern string) { func (engine *Engine) LoadHTMLFiles(files ...string) { if IsDebugging() { - render.HTMLDebug.AddFiles(files...) - engine.HTMLRender = render.HTMLDebug + engine.HTMLRender = render.HTMLDebug{Files: files} } else { templ := template.Must(template.ParseFiles(files...)) engine.SetHTMLTemplate(templ) @@ -86,9 +116,15 @@ func (engine *Engine) LoadHTMLFiles(files ...string) { } func (engine *Engine) SetHTMLTemplate(templ *template.Template) { - engine.HTMLRender = render.HTMLRender{ - Template: templ, + if len(engine.trees) > 0 { + debugPrint(`[WARNING] Since SetHTMLTemplate() is NOT thread-safe. It should only be called +at initialization. ie. before any route is registered or the router is listening in a socket: + + router := gin.Default() + router.SetHTMLTemplate(template) // << good place +`) } + engine.HTMLRender = render.HTMLProduction{Template: templ} } // Adds handlers for NoRoute. It return a 404 code by default. @@ -97,45 +133,203 @@ func (engine *Engine) NoRoute(handlers ...HandlerFunc) { engine.rebuild404Handlers() } -func (engine *Engine) Use(middlewares ...HandlerFunc) { - engine.RouterGroup.Use(middlewares...) +// Sets the handlers called when... TODO +func (engine *Engine) NoMethod(handlers ...HandlerFunc) { + engine.noMethod = handlers + engine.rebuild405Handlers() +} + +// Attachs a global middleware to the router. ie. the middleware attached though Use() will be +// included in the handlers chain for every single request. Even 404, 405, static files... +// For example, this is the right place for a logger or error management middleware. +func (engine *Engine) Use(middleware ...HandlerFunc) routesInterface { + engine.RouterGroup.Use(middleware...) engine.rebuild404Handlers() + engine.rebuild405Handlers() + return engine } func (engine *Engine) rebuild404Handlers() { engine.allNoRoute = engine.combineHandlers(engine.noRoute) } -func (engine *Engine) handle404(w http.ResponseWriter, req *http.Request) { - c := engine.createContext(w, req, nil, engine.allNoRoute) - // set 404 by default, useful for logging - c.Writer.WriteHeader(404) - c.Next() - if !c.Writer.Written() { - if c.Writer.Status() == 404 { - c.Data(-1, MIMEPlain, engine.Default404Body) - } else { - c.Writer.WriteHeaderNow() +func (engine *Engine) rebuild405Handlers() { + engine.allNoMethod = engine.combineHandlers(engine.noMethod) +} + +func (engine *Engine) addRoute(method, path string, handlers HandlersChain) { + debugPrintRoute(method, path, handlers) + + if path[0] != '/' { + panic("path must begin with '/'") + } + if method == "" { + panic("HTTP method can not be empty") + } + if len(handlers) == 0 { + panic("there must be at least one handler") + } + + root := engine.trees.get(method) + if root == nil { + root = new(node) + engine.trees = append(engine.trees, methodTree{ + method: method, + root: root, + }) + } + root.addRoute(path, handlers) +} + +// The router is attached to a http.Server and starts listening and serving HTTP requests. +// It is a shortcut for http.ListenAndServe(addr, router) +// Note: this method will block the calling goroutine undefinitelly unless an error happens. +func (engine *Engine) Run(addr string) (err error) { + debugPrint("Listening and serving HTTP on %s\n", addr) + defer func() { debugPrintError(err) }() + + err = http.ListenAndServe(addr, engine) + return +} + +// The router is attached to a http.Server and starts listening and serving HTTPS requests. +// It is a shortcut for http.ListenAndServeTLS(addr, certFile, keyFile, router) +// Note: this method will block the calling goroutine undefinitelly unless an error happens. +func (engine *Engine) RunTLS(addr string, certFile string, keyFile string) (err error) { + debugPrint("Listening and serving HTTPS on %s\n", addr) + defer func() { debugPrintError(err) }() + + err = http.ListenAndServeTLS(addr, certFile, keyFile, engine) + return +} + +// The router is attached to a http.Server and starts listening and serving HTTP requests +// through the specified unix socket (ie. a file) +// Note: this method will block the calling goroutine undefinitelly unless an error happens. +func (engine *Engine) RunUnix(file string) (err error) { + debugPrint("Listening and serving HTTP on unix:/%s", file) + defer func() { debugPrintError(err) }() + + os.Remove(file) + listener, err := net.Listen("unix", file) + if err != nil { + return + } + defer listener.Close() + err = http.Serve(listener, engine) + return +} + +// Conforms to the http.Handler interface. +func (engine *Engine) ServeHTTP(w http.ResponseWriter, req *http.Request) { + c := engine.pool.Get().(*Context) + c.writermem.reset(w) + c.Request = req + c.reset() + + engine.handleHTTPRequest(c) + + engine.pool.Put(c) +} + +func (engine *Engine) handleHTTPRequest(context *Context) { + httpMethod := context.Request.Method + path := context.Request.URL.Path + + // Find root of the tree for the given HTTP method + t := engine.trees + for i, tl := 0, len(t); i < tl; i++ { + if t[i].method == httpMethod { + root := t[i].root + // Find route in tree + handlers, params, tsr := root.getValue(path, context.Params) + if handlers != nil { + context.handlers = handlers + context.Params = params + context.Next() + context.writermem.WriteHeaderNow() + return + + } else if httpMethod != "CONNECT" && path != "/" { + if tsr && engine.RedirectFixedPath { + redirectTrailingSlash(context) + return + } + if engine.RedirectFixedPath && redirectFixedPath(context, root, engine.RedirectFixedPath) { + return + } + } + break } } - engine.reuseContext(c) + + // TODO: unit test + if engine.HandleMethodNotAllowed { + for _, tree := range engine.trees { + if tree.method != httpMethod { + if handlers, _, _ := tree.root.getValue(path, nil); handlers != nil { + context.handlers = engine.allNoMethod + serveError(context, 405, default405Body) + return + } + } + } + } + context.handlers = engine.allNoRoute + serveError(context, 404, default404Body) } -// ServeHTTP makes the router implement the http.Handler interface. -func (engine *Engine) ServeHTTP(writer http.ResponseWriter, request *http.Request) { - engine.router.ServeHTTP(writer, request) -} +var mimePlain = []string{MIMEPlain} -func (engine *Engine) Run(addr string) { - debugPrint("Listening and serving HTTP on %s", addr) - if err := http.ListenAndServe(addr, engine); err != nil { - panic(err) +func serveError(c *Context, code int, defaultMessage []byte) { + c.writermem.status = code + c.Next() + if !c.writermem.Written() { + if c.writermem.Status() == code { + c.writermem.Header()["Content-Type"] = mimePlain + c.Writer.Write(defaultMessage) + } else { + c.writermem.WriteHeaderNow() + } } } -func (engine *Engine) RunTLS(addr string, cert string, key string) { - debugPrint("Listening and serving HTTPS on %s", addr) - if err := http.ListenAndServeTLS(addr, cert, key, engine); err != nil { - panic(err) +func redirectTrailingSlash(c *Context) { + req := c.Request + path := req.URL.Path + code := 301 // Permanent redirect, request with GET method + if req.Method != "GET" { + code = 307 } + + if len(path) > 1 && path[len(path)-1] == '/' { + req.URL.Path = path[:len(path)-1] + } else { + req.URL.Path = path + "/" + } + debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) + http.Redirect(c.Writer, req, req.URL.String(), code) + c.writermem.WriteHeaderNow() +} + +func redirectFixedPath(c *Context, root *node, trailingSlash bool) bool { + req := c.Request + path := req.URL.Path + + fixedPath, found := root.findCaseInsensitivePath( + cleanPath(path), + trailingSlash, + ) + if found { + code := 301 // Permanent redirect, request with GET method + if req.Method != "GET" { + code = 307 + } + req.URL.Path = string(fixedPath) + debugPrint("redirecting request %d: %s --> %s", code, path, req.URL.String()) + http.Redirect(c.Writer, req, req.URL.String(), code) + c.writermem.WriteHeaderNow() + return true + } + return false } diff --git a/gin_integration_test.go b/gin_integration_test.go new file mode 100644 index 00000000..f7ae0758 --- /dev/null +++ b/gin_integration_test.go @@ -0,0 +1,69 @@ +package gin + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "net" + "net/http" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestRun(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + go func() { + router.Use(LoggerWithWriter(buffer)) + router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) + router.Run(":5150") + }() + // have to wait for the goroutine to start and run the server + // otherwise the main thread will complete + time.Sleep(5 * time.Millisecond) + + assert.Error(t, router.Run(":5150")) + + resp, err := http.Get("http://localhost:5150/example") + defer resp.Body.Close() + assert.NoError(t, err) + + body, ioerr := ioutil.ReadAll(resp.Body) + assert.NoError(t, ioerr) + assert.Equal(t, "it worked", string(body[:]), "resp body should match") + assert.Equal(t, "200 OK", resp.Status, "should get a 200") +} + +func TestUnixSocket(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + + go func() { + router.Use(LoggerWithWriter(buffer)) + router.GET("/example", func(c *Context) { c.String(http.StatusOK, "it worked") }) + router.RunUnix("/tmp/unix_unit_test") + }() + // have to wait for the goroutine to start and run the server + // otherwise the main thread will complete + time.Sleep(5 * time.Millisecond) + + c, err := net.Dial("unix", "/tmp/unix_unit_test") + assert.NoError(t, err) + + fmt.Fprintf(c, "GET /example HTTP/1.0\r\n\r\n") + scanner := bufio.NewScanner(c) + var response string + for scanner.Scan() { + response += scanner.Text() + } + assert.Contains(t, response, "HTTP/1.0 200", "should get a 200") + assert.Contains(t, response, "it worked", "resp body should match") +} + +func TestBadUnixSocket(t *testing.T) { + router := New() + assert.Error(t, router.RunUnix("#/tmp/unix_unit_test")) +} diff --git a/gin_test.go b/gin_test.go index 1368aa08..28bba734 100644 --- a/gin_test.go +++ b/gin_test.go @@ -5,203 +5,178 @@ package gin import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "os" - "path" - "strings" + "reflect" "testing" + + "github.com/stretchr/testify/assert" ) +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + func init() { SetMode(TestMode) } -func PerformRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { - req, _ := http.NewRequest(method, path, nil) - w := httptest.NewRecorder() - r.ServeHTTP(w, req) - return w +func TestCreateEngine(t *testing.T) { + router := New() + assert.Equal(t, "/", router.BasePath) + assert.Equal(t, router.engine, router) + assert.Empty(t, router.Handlers) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) +func TestAddRoute(t *testing.T) { + router := New() + router.addRoute("GET", "/", HandlersChain{func(_ *Context) {}}) - // RUN - w := PerformRequest(r, method, "/test") + assert.Len(t, router.trees, 1) + assert.NotNil(t, router.trees.get("GET")) + assert.Nil(t, router.trees.get("POST")) - // TEST - if passed == false { - t.Errorf(method + " route handler was not invoked.") - } - if w.Code != http.StatusOK { - t.Errorf("Status code should be %v, was %d", http.StatusOK, w.Code) - } -} -func TestRouterGroupRouteOK(t *testing.T) { - testRouteOK("POST", t) - testRouteOK("DELETE", t) - testRouteOK("PATCH", t) - testRouteOK("PUT", t) - testRouteOK("OPTIONS", t) - testRouteOK("HEAD", t) + router.addRoute("POST", "/", HandlersChain{func(_ *Context) {}}) + + assert.Len(t, router.trees, 2) + assert.NotNil(t, router.trees.get("GET")) + assert.NotNil(t, router.trees.get("POST")) + + router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}}) + assert.Len(t, router.trees, 2) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK(method string, t *testing.T) { - // SETUP - passed := false - r := New() - r.Handle(method, "/test_2", []HandlerFunc{func(c *Context) { - passed = true - }}) +func TestAddRouteFails(t *testing.T) { + router := New() + assert.Panics(t, func() { router.addRoute("", "/", HandlersChain{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.addRoute("GET", "a", HandlersChain{func(_ *Context) {}}) }) + assert.Panics(t, func() { router.addRoute("GET", "/", HandlersChain{}) }) - // RUN - w := PerformRequest(r, method, "/test") - - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusNotFound { - // If this fails, it's because httprouter needs to be updated to at least f78f58a0db - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location")) - } + router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}}) + assert.Panics(t, func() { + router.addRoute("POST", "/post", HandlersChain{func(_ *Context) {}}) + }) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK(t *testing.T) { - testRouteNotOK("POST", t) - testRouteNotOK("DELETE", t) - testRouteNotOK("PATCH", t) - testRouteNotOK("PUT", t) - testRouteNotOK("OPTIONS", t) - testRouteNotOK("HEAD", t) +func TestCreateDefaultRouter(t *testing.T) { + router := Default() + assert.Len(t, router.Handlers, 2) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func testRouteNotOK2(method string, t *testing.T) { - // SETUP - passed := false - r := New() - var methodRoute string - if method == "POST" { - methodRoute = "GET" - } else { - methodRoute = "POST" - } - r.Handle(methodRoute, "/test", []HandlerFunc{func(c *Context) { - passed = true - }}) +func TestNoRouteWithoutGlobalHandlers(t *testing.T) { + var middleware0 HandlerFunc = func(c *Context) {} + var middleware1 HandlerFunc = func(c *Context) {} - // RUN - w := PerformRequest(r, method, "/test") + router := New() - // TEST - if passed == true { - t.Errorf(method + " route handler was invoked, when it should not") - } - if w.Code != http.StatusNotFound { - // If this fails, it's because httprouter needs to be updated to at least f78f58a0db - t.Errorf("Status code should be %v, was %d. Location: %s", http.StatusNotFound, w.Code, w.HeaderMap.Get("Location")) - } + router.NoRoute(middleware0) + assert.Nil(t, router.Handlers) + assert.Len(t, router.noRoute, 1) + assert.Len(t, router.allNoRoute, 1) + compareFunc(t, router.noRoute[0], middleware0) + compareFunc(t, router.allNoRoute[0], middleware0) + + router.NoRoute(middleware1, middleware0) + assert.Len(t, router.noRoute, 2) + assert.Len(t, router.allNoRoute, 2) + compareFunc(t, router.noRoute[0], middleware1) + compareFunc(t, router.allNoRoute[0], middleware1) + compareFunc(t, router.noRoute[1], middleware0) + compareFunc(t, router.allNoRoute[1], middleware0) } -// TestSingleRouteOK tests that POST route is correctly invoked. -func TestRouteNotOK2(t *testing.T) { - testRouteNotOK2("POST", t) - testRouteNotOK2("DELETE", t) - testRouteNotOK2("PATCH", t) - testRouteNotOK2("PUT", t) - testRouteNotOK2("OPTIONS", t) - testRouteNotOK2("HEAD", t) +func TestNoRouteWithGlobalHandlers(t *testing.T) { + var middleware0 HandlerFunc = func(c *Context) {} + var middleware1 HandlerFunc = func(c *Context) {} + var middleware2 HandlerFunc = func(c *Context) {} + + router := New() + router.Use(middleware2) + + router.NoRoute(middleware0) + assert.Len(t, router.allNoRoute, 2) + assert.Len(t, router.Handlers, 1) + assert.Len(t, router.noRoute, 1) + + compareFunc(t, router.Handlers[0], middleware2) + compareFunc(t, router.noRoute[0], middleware0) + compareFunc(t, router.allNoRoute[0], middleware2) + compareFunc(t, router.allNoRoute[1], middleware0) + + router.Use(middleware1) + assert.Len(t, router.allNoRoute, 3) + assert.Len(t, router.Handlers, 2) + assert.Len(t, router.noRoute, 1) + + compareFunc(t, router.Handlers[0], middleware2) + compareFunc(t, router.Handlers[1], middleware1) + compareFunc(t, router.noRoute[0], middleware0) + compareFunc(t, router.allNoRoute[0], middleware2) + compareFunc(t, router.allNoRoute[1], middleware1) + compareFunc(t, router.allNoRoute[2], middleware0) } -// TestHandleStaticFile - ensure the static file handles properly -func TestHandleStaticFile(t *testing.T) { - // SETUP file - testRoot, _ := os.Getwd() - f, err := ioutil.TempFile(testRoot, "") - if err != nil { - t.Error(err) - } - defer os.Remove(f.Name()) - filePath := path.Join("/", path.Base(f.Name())) - f.WriteString("Gin Web Framework") - f.Close() +func TestNoMethodWithoutGlobalHandlers(t *testing.T) { + var middleware0 HandlerFunc = func(c *Context) {} + var middleware1 HandlerFunc = func(c *Context) {} - // SETUP gin - r := New() - r.Static("./", testRoot) + router := New() - // RUN - w := PerformRequest(r, "GET", filePath) + router.NoMethod(middleware0) + assert.Empty(t, router.Handlers) + assert.Len(t, router.noMethod, 1) + assert.Len(t, router.allNoMethod, 1) + compareFunc(t, router.noMethod[0], middleware0) + compareFunc(t, router.allNoMethod[0], middleware0) - // TEST - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if w.Body.String() != "Gin Web Framework" { - t.Errorf("Response should be test, was: %s", w.Body.String()) - } - if w.HeaderMap.Get("Content-Type") != "text/plain; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } + router.NoMethod(middleware1, middleware0) + assert.Len(t, router.noMethod, 2) + assert.Len(t, router.allNoMethod, 2) + compareFunc(t, router.noMethod[0], middleware1) + compareFunc(t, router.allNoMethod[0], middleware1) + compareFunc(t, router.noMethod[1], middleware0) + compareFunc(t, router.allNoMethod[1], middleware0) } -// TestHandleStaticDir - ensure the root/sub dir handles properly -func TestHandleStaticDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") +func TestRebuild404Handlers(t *testing.T) { - // RUN - w := PerformRequest(r, "GET", "/") - - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be 200, was: %d", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) - } } -// TestHandleHeadToDir - ensure the root/sub dir handles properly -func TestHandleHeadToDir(t *testing.T) { - // SETUP - r := New() - r.Static("/", "./") +func TestNoMethodWithGlobalHandlers(t *testing.T) { + var middleware0 HandlerFunc = func(c *Context) {} + var middleware1 HandlerFunc = func(c *Context) {} + var middleware2 HandlerFunc = func(c *Context) {} - // RUN - w := PerformRequest(r, "HEAD", "/") + router := New() + router.Use(middleware2) - // TEST - bodyAsString := w.Body.String() - if w.Code != 200 { - t.Errorf("Response code should be Ok, was: %s", w.Code) - } - if len(bodyAsString) == 0 { - t.Errorf("Got empty body instead of file tree") - } - if !strings.Contains(bodyAsString, "gin.go") { - t.Errorf("Can't find:`gin.go` in file tree: %s", bodyAsString) - } - if w.HeaderMap.Get("Content-Type") != "text/html; charset=utf-8" { - t.Errorf("Content-Type should be text/plain, was %s", w.HeaderMap.Get("Content-Type")) + router.NoMethod(middleware0) + assert.Len(t, router.allNoMethod, 2) + assert.Len(t, router.Handlers, 1) + assert.Len(t, router.noMethod, 1) + + compareFunc(t, router.Handlers[0], middleware2) + compareFunc(t, router.noMethod[0], middleware0) + compareFunc(t, router.allNoMethod[0], middleware2) + compareFunc(t, router.allNoMethod[1], middleware0) + + router.Use(middleware1) + assert.Len(t, router.allNoMethod, 3) + assert.Len(t, router.Handlers, 2) + assert.Len(t, router.noMethod, 1) + + compareFunc(t, router.Handlers[0], middleware2) + compareFunc(t, router.Handlers[1], middleware1) + compareFunc(t, router.noMethod[0], middleware0) + compareFunc(t, router.allNoMethod[0], middleware2) + compareFunc(t, router.allNoMethod[1], middleware1) + compareFunc(t, router.allNoMethod[2], middleware0) +} + +func compareFunc(t *testing.T, a, b interface{}) { + sf1 := reflect.ValueOf(a) + sf2 := reflect.ValueOf(b) + if sf1.Pointer() != sf2.Pointer() { + t.Error("different functions") } } diff --git a/githubapi_test.go b/githubapi_test.go new file mode 100644 index 00000000..2227fa6a --- /dev/null +++ b/githubapi_test.go @@ -0,0 +1,389 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "fmt" + "math/rand" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +type route struct { + method string + path string +} + +// http://developer.github.com/v3/ +var githubAPI = []route{ + // OAuth Authorizations + {"GET", "/authorizations"}, + {"GET", "/authorizations/:id"}, + {"POST", "/authorizations"}, + //{"PUT", "/authorizations/clients/:client_id"}, + //{"PATCH", "/authorizations/:id"}, + {"DELETE", "/authorizations/:id"}, + {"GET", "/applications/:client_id/tokens/:access_token"}, + {"DELETE", "/applications/:client_id/tokens"}, + {"DELETE", "/applications/:client_id/tokens/:access_token"}, + + // Activity + {"GET", "/events"}, + {"GET", "/repos/:owner/:repo/events"}, + {"GET", "/networks/:owner/:repo/events"}, + {"GET", "/orgs/:org/events"}, + {"GET", "/users/:user/received_events"}, + {"GET", "/users/:user/received_events/public"}, + {"GET", "/users/:user/events"}, + {"GET", "/users/:user/events/public"}, + {"GET", "/users/:user/events/orgs/:org"}, + {"GET", "/feeds"}, + {"GET", "/notifications"}, + {"GET", "/repos/:owner/:repo/notifications"}, + {"PUT", "/notifications"}, + {"PUT", "/repos/:owner/:repo/notifications"}, + {"GET", "/notifications/threads/:id"}, + //{"PATCH", "/notifications/threads/:id"}, + {"GET", "/notifications/threads/:id/subscription"}, + {"PUT", "/notifications/threads/:id/subscription"}, + {"DELETE", "/notifications/threads/:id/subscription"}, + {"GET", "/repos/:owner/:repo/stargazers"}, + {"GET", "/users/:user/starred"}, + {"GET", "/user/starred"}, + {"GET", "/user/starred/:owner/:repo"}, + {"PUT", "/user/starred/:owner/:repo"}, + {"DELETE", "/user/starred/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/subscribers"}, + {"GET", "/users/:user/subscriptions"}, + {"GET", "/user/subscriptions"}, + {"GET", "/repos/:owner/:repo/subscription"}, + {"PUT", "/repos/:owner/:repo/subscription"}, + {"DELETE", "/repos/:owner/:repo/subscription"}, + {"GET", "/user/subscriptions/:owner/:repo"}, + {"PUT", "/user/subscriptions/:owner/:repo"}, + {"DELETE", "/user/subscriptions/:owner/:repo"}, + + // Gists + {"GET", "/users/:user/gists"}, + {"GET", "/gists"}, + //{"GET", "/gists/public"}, + //{"GET", "/gists/starred"}, + {"GET", "/gists/:id"}, + {"POST", "/gists"}, + //{"PATCH", "/gists/:id"}, + {"PUT", "/gists/:id/star"}, + {"DELETE", "/gists/:id/star"}, + {"GET", "/gists/:id/star"}, + {"POST", "/gists/:id/forks"}, + {"DELETE", "/gists/:id"}, + + // Git Data + {"GET", "/repos/:owner/:repo/git/blobs/:sha"}, + {"POST", "/repos/:owner/:repo/git/blobs"}, + {"GET", "/repos/:owner/:repo/git/commits/:sha"}, + {"POST", "/repos/:owner/:repo/git/commits"}, + //{"GET", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/refs"}, + {"POST", "/repos/:owner/:repo/git/refs"}, + //{"PATCH", "/repos/:owner/:repo/git/refs/*ref"}, + //{"DELETE", "/repos/:owner/:repo/git/refs/*ref"}, + {"GET", "/repos/:owner/:repo/git/tags/:sha"}, + {"POST", "/repos/:owner/:repo/git/tags"}, + {"GET", "/repos/:owner/:repo/git/trees/:sha"}, + {"POST", "/repos/:owner/:repo/git/trees"}, + + // Issues + {"GET", "/issues"}, + {"GET", "/user/issues"}, + {"GET", "/orgs/:org/issues"}, + {"GET", "/repos/:owner/:repo/issues"}, + {"GET", "/repos/:owner/:repo/issues/:number"}, + {"POST", "/repos/:owner/:repo/issues"}, + //{"PATCH", "/repos/:owner/:repo/issues/:number"}, + {"GET", "/repos/:owner/:repo/assignees"}, + {"GET", "/repos/:owner/:repo/assignees/:assignee"}, + {"GET", "/repos/:owner/:repo/issues/:number/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments"}, + //{"GET", "/repos/:owner/:repo/issues/comments/:id"}, + {"POST", "/repos/:owner/:repo/issues/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/issues/comments/:id"}, + //{"DELETE", "/repos/:owner/:repo/issues/comments/:id"}, + {"GET", "/repos/:owner/:repo/issues/:number/events"}, + //{"GET", "/repos/:owner/:repo/issues/events"}, + //{"GET", "/repos/:owner/:repo/issues/events/:id"}, + {"GET", "/repos/:owner/:repo/labels"}, + {"GET", "/repos/:owner/:repo/labels/:name"}, + {"POST", "/repos/:owner/:repo/labels"}, + //{"PATCH", "/repos/:owner/:repo/labels/:name"}, + {"DELETE", "/repos/:owner/:repo/labels/:name"}, + {"GET", "/repos/:owner/:repo/issues/:number/labels"}, + {"POST", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels/:name"}, + {"PUT", "/repos/:owner/:repo/issues/:number/labels"}, + {"DELETE", "/repos/:owner/:repo/issues/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones/:number/labels"}, + {"GET", "/repos/:owner/:repo/milestones"}, + {"GET", "/repos/:owner/:repo/milestones/:number"}, + {"POST", "/repos/:owner/:repo/milestones"}, + //{"PATCH", "/repos/:owner/:repo/milestones/:number"}, + {"DELETE", "/repos/:owner/:repo/milestones/:number"}, + + // Miscellaneous + {"GET", "/emojis"}, + {"GET", "/gitignore/templates"}, + {"GET", "/gitignore/templates/:name"}, + {"POST", "/markdown"}, + {"POST", "/markdown/raw"}, + {"GET", "/meta"}, + {"GET", "/rate_limit"}, + + // Organizations + {"GET", "/users/:user/orgs"}, + {"GET", "/user/orgs"}, + {"GET", "/orgs/:org"}, + //{"PATCH", "/orgs/:org"}, + {"GET", "/orgs/:org/members"}, + {"GET", "/orgs/:org/members/:user"}, + {"DELETE", "/orgs/:org/members/:user"}, + {"GET", "/orgs/:org/public_members"}, + {"GET", "/orgs/:org/public_members/:user"}, + {"PUT", "/orgs/:org/public_members/:user"}, + {"DELETE", "/orgs/:org/public_members/:user"}, + {"GET", "/orgs/:org/teams"}, + {"GET", "/teams/:id"}, + {"POST", "/orgs/:org/teams"}, + //{"PATCH", "/teams/:id"}, + {"DELETE", "/teams/:id"}, + {"GET", "/teams/:id/members"}, + {"GET", "/teams/:id/members/:user"}, + {"PUT", "/teams/:id/members/:user"}, + {"DELETE", "/teams/:id/members/:user"}, + {"GET", "/teams/:id/repos"}, + {"GET", "/teams/:id/repos/:owner/:repo"}, + {"PUT", "/teams/:id/repos/:owner/:repo"}, + {"DELETE", "/teams/:id/repos/:owner/:repo"}, + {"GET", "/user/teams"}, + + // Pull Requests + {"GET", "/repos/:owner/:repo/pulls"}, + {"GET", "/repos/:owner/:repo/pulls/:number"}, + {"POST", "/repos/:owner/:repo/pulls"}, + //{"PATCH", "/repos/:owner/:repo/pulls/:number"}, + {"GET", "/repos/:owner/:repo/pulls/:number/commits"}, + {"GET", "/repos/:owner/:repo/pulls/:number/files"}, + {"GET", "/repos/:owner/:repo/pulls/:number/merge"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/merge"}, + {"GET", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments"}, + //{"GET", "/repos/:owner/:repo/pulls/comments/:number"}, + {"PUT", "/repos/:owner/:repo/pulls/:number/comments"}, + //{"PATCH", "/repos/:owner/:repo/pulls/comments/:number"}, + //{"DELETE", "/repos/:owner/:repo/pulls/comments/:number"}, + + // Repositories + {"GET", "/user/repos"}, + {"GET", "/users/:user/repos"}, + {"GET", "/orgs/:org/repos"}, + {"GET", "/repositories"}, + {"POST", "/user/repos"}, + {"POST", "/orgs/:org/repos"}, + {"GET", "/repos/:owner/:repo"}, + //{"PATCH", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/contributors"}, + {"GET", "/repos/:owner/:repo/languages"}, + {"GET", "/repos/:owner/:repo/teams"}, + {"GET", "/repos/:owner/:repo/tags"}, + {"GET", "/repos/:owner/:repo/branches"}, + {"GET", "/repos/:owner/:repo/branches/:branch"}, + {"DELETE", "/repos/:owner/:repo"}, + {"GET", "/repos/:owner/:repo/collaborators"}, + {"GET", "/repos/:owner/:repo/collaborators/:user"}, + {"PUT", "/repos/:owner/:repo/collaborators/:user"}, + {"DELETE", "/repos/:owner/:repo/collaborators/:user"}, + {"GET", "/repos/:owner/:repo/comments"}, + {"GET", "/repos/:owner/:repo/commits/:sha/comments"}, + {"POST", "/repos/:owner/:repo/commits/:sha/comments"}, + {"GET", "/repos/:owner/:repo/comments/:id"}, + //{"PATCH", "/repos/:owner/:repo/comments/:id"}, + {"DELETE", "/repos/:owner/:repo/comments/:id"}, + {"GET", "/repos/:owner/:repo/commits"}, + {"GET", "/repos/:owner/:repo/commits/:sha"}, + {"GET", "/repos/:owner/:repo/readme"}, + //{"GET", "/repos/:owner/:repo/contents/*path"}, + //{"PUT", "/repos/:owner/:repo/contents/*path"}, + //{"DELETE", "/repos/:owner/:repo/contents/*path"}, + //{"GET", "/repos/:owner/:repo/:archive_format/:ref"}, + {"GET", "/repos/:owner/:repo/keys"}, + {"GET", "/repos/:owner/:repo/keys/:id"}, + {"POST", "/repos/:owner/:repo/keys"}, + //{"PATCH", "/repos/:owner/:repo/keys/:id"}, + {"DELETE", "/repos/:owner/:repo/keys/:id"}, + {"GET", "/repos/:owner/:repo/downloads"}, + {"GET", "/repos/:owner/:repo/downloads/:id"}, + {"DELETE", "/repos/:owner/:repo/downloads/:id"}, + {"GET", "/repos/:owner/:repo/forks"}, + {"POST", "/repos/:owner/:repo/forks"}, + {"GET", "/repos/:owner/:repo/hooks"}, + {"GET", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks"}, + //{"PATCH", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/hooks/:id/tests"}, + {"DELETE", "/repos/:owner/:repo/hooks/:id"}, + {"POST", "/repos/:owner/:repo/merges"}, + {"GET", "/repos/:owner/:repo/releases"}, + {"GET", "/repos/:owner/:repo/releases/:id"}, + {"POST", "/repos/:owner/:repo/releases"}, + //{"PATCH", "/repos/:owner/:repo/releases/:id"}, + {"DELETE", "/repos/:owner/:repo/releases/:id"}, + {"GET", "/repos/:owner/:repo/releases/:id/assets"}, + {"GET", "/repos/:owner/:repo/stats/contributors"}, + {"GET", "/repos/:owner/:repo/stats/commit_activity"}, + {"GET", "/repos/:owner/:repo/stats/code_frequency"}, + {"GET", "/repos/:owner/:repo/stats/participation"}, + {"GET", "/repos/:owner/:repo/stats/punch_card"}, + {"GET", "/repos/:owner/:repo/statuses/:ref"}, + {"POST", "/repos/:owner/:repo/statuses/:ref"}, + + // Search + {"GET", "/search/repositories"}, + {"GET", "/search/code"}, + {"GET", "/search/issues"}, + {"GET", "/search/users"}, + {"GET", "/legacy/issues/search/:owner/:repository/:state/:keyword"}, + {"GET", "/legacy/repos/search/:keyword"}, + {"GET", "/legacy/user/search/:keyword"}, + {"GET", "/legacy/user/email/:email"}, + + // Users + {"GET", "/users/:user"}, + {"GET", "/user"}, + //{"PATCH", "/user"}, + {"GET", "/users"}, + {"GET", "/user/emails"}, + {"POST", "/user/emails"}, + {"DELETE", "/user/emails"}, + {"GET", "/users/:user/followers"}, + {"GET", "/user/followers"}, + {"GET", "/users/:user/following"}, + {"GET", "/user/following"}, + {"GET", "/user/following/:user"}, + {"GET", "/users/:user/following/:target_user"}, + {"PUT", "/user/following/:user"}, + {"DELETE", "/user/following/:user"}, + {"GET", "/users/:user/keys"}, + {"GET", "/user/keys"}, + {"GET", "/user/keys/:id"}, + {"POST", "/user/keys"}, + //{"PATCH", "/user/keys/:id"}, + {"DELETE", "/user/keys/:id"}, +} + +func githubConfigRouter(router *Engine) { + for _, route := range githubAPI { + router.Handle(route.method, route.path, func(c *Context) { + output := make(map[string]string, len(c.Params)+1) + output["status"] = "good" + for _, param := range c.Params { + output[param.Key] = param.Value + } + c.JSON(200, output) + }) + } +} + +func TestGithubAPI(t *testing.T) { + DefaultWriter = newMockWriter() + router := Default() + githubConfigRouter(router) + + for _, route := range githubAPI { + path, values := exampleFromPath(route.path) + w := performRequest(router, route.method, path) + + // TEST + assert.Contains(t, w.Body.String(), "\"status\":\"good\"") + for _, value := range values { + str := fmt.Sprintf("\"%s\":\"%s\"", value.Key, value.Value) + assert.Contains(t, w.Body.String(), str) + } + } +} + +func exampleFromPath(path string) (string, Params) { + output := new(bytes.Buffer) + params := make(Params, 0, 6) + start := -1 + for i, c := range path { + if c == ':' { + start = i + 1 + } + if start >= 0 { + if c == '/' { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:i], + Value: value, + }) + output.WriteString(value) + output.WriteRune(c) + start = -1 + } + } else { + output.WriteRune(c) + } + } + if start >= 0 { + value := fmt.Sprint(rand.Intn(100000)) + params = append(params, Param{ + Key: path[start:len(path)], + Value: value, + }) + output.WriteString(value) + } + + return output.String(), params +} + +func BenchmarkGithub(b *testing.B) { + router := New() + githubConfigRouter(router) + runRequest(b, router, "GET", "/legacy/issues/search/:owner/:repository/:state/:keyword") +} + +func BenchmarkParallelGithub(b *testing.B) { + DefaultWriter = newMockWriter() + router := New() + githubConfigRouter(router) + + req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) + + b.RunParallel(func(pb *testing.PB) { + // Each goroutine has its own bytes.Buffer. + for pb.Next() { + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + } + }) +} + +func BenchmarkParallelGithubDefault(b *testing.B) { + DefaultWriter = newMockWriter() + router := Default() + githubConfigRouter(router) + + req, _ := http.NewRequest("POST", "/repos/manucorporat/sse/git/blobs", nil) + + b.RunParallel(func(pb *testing.PB) { + // Each goroutine has its own bytes.Buffer. + for pb.Next() { + w := httptest.NewRecorder() + router.ServeHTTP(w, req) + } + }) +} diff --git a/logger.go b/logger.go index 5054f6ec..e0f9b367 100644 --- a/logger.go +++ b/logger.go @@ -5,8 +5,8 @@ package gin import ( - "log" - "os" + "fmt" + "io" "time" ) @@ -22,28 +22,35 @@ var ( ) func ErrorLogger() HandlerFunc { - return ErrorLoggerT(ErrorTypeAll) + return ErrorLoggerT(ErrorTypeAny) } -func ErrorLoggerT(typ uint32) HandlerFunc { +func ErrorLoggerT(typ ErrorType) HandlerFunc { return func(c *Context) { c.Next() - - errs := c.Errors.ByType(typ) - if len(errs) > 0 { - // -1 status code = do not change current one - c.JSON(-1, c.Errors) + // avoid writting if we already wrote into the response body + if !c.Writer.Written() { + errors := c.Errors.ByType(typ) + if len(errors) > 0 { + c.JSON(-1, errors) + } } } } +// Instances a Logger middleware that will write the logs to gin.DefaultWriter +// By default gin.DefaultWriter = os.Stdout func Logger() HandlerFunc { - stdlogger := log.New(os.Stdout, "", 0) - //errlogger := log.New(os.Stderr, "", 0) + return LoggerWithWriter(DefaultWriter) +} +// Instance a Logger middleware with the specified writter buffer. +// Example: os.Stdout, a file opened in write mode, a socket... +func LoggerWithWriter(out io.Writer) HandlerFunc { return func(c *Context) { // Start timer start := time.Now() + path := c.Request.URL.Path // Process request c.Next() @@ -57,26 +64,27 @@ func Logger() HandlerFunc { statusCode := c.Writer.Status() statusColor := colorForStatus(statusCode) methodColor := colorForMethod(method) + comment := c.Errors.ByType(ErrorTypePrivate).String() - stdlogger.Printf("[GIN] %v |%s %3d %s| %12v | %s |%s %s %-7s %s\n%s", + fmt.Fprintf(out, "[GIN] %v |%s %3d %s| %13v | %s |%s %s %-7s %s\n%s", end.Format("2006/01/02 - 15:04:05"), statusColor, statusCode, reset, latency, clientIP, methodColor, reset, method, - c.Request.URL.Path, - c.Errors.String(), + path, + comment, ) } } func colorForStatus(code int) string { switch { - case code >= 200 && code <= 299: + case code >= 200 && code < 300: return green - case code >= 300 && code <= 399: + case code >= 300 && code < 400: return white - case code >= 400 && code <= 499: + case code >= 400 && code < 500: return yellow default: return red @@ -84,20 +92,20 @@ func colorForStatus(code int) string { } func colorForMethod(method string) string { - switch { - case method == "GET": + switch method { + case "GET": return blue - case method == "POST": + case "POST": return cyan - case method == "PUT": + case "PUT": return yellow - case method == "DELETE": + case "DELETE": return red - case method == "PATCH": + case "PATCH": return green - case method == "HEAD": + case "HEAD": return magenta - case method == "OPTIONS": + case "OPTIONS": return white default: return reset diff --git a/logger_test.go b/logger_test.go new file mode 100644 index 00000000..1cdaa94c --- /dev/null +++ b/logger_test.go @@ -0,0 +1,98 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "bytes" + "testing" + + "github.com/stretchr/testify/assert" +) + +//TODO +// func (engine *Engine) LoadHTMLGlob(pattern string) { +// func (engine *Engine) LoadHTMLFiles(files ...string) { +// func (engine *Engine) Run(addr string) error { +// func (engine *Engine) RunTLS(addr string, cert string, key string) error { + +func init() { + SetMode(TestMode) +} + +func TestLogger(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + router.Use(LoggerWithWriter(buffer)) + router.GET("/example", func(c *Context) {}) + router.POST("/example", func(c *Context) {}) + router.PUT("/example", func(c *Context) {}) + router.DELETE("/example", func(c *Context) {}) + router.PATCH("/example", func(c *Context) {}) + router.HEAD("/example", func(c *Context) {}) + router.OPTIONS("/example", func(c *Context) {}) + + performRequest(router, "GET", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/example") + + // I wrote these first (extending the above) but then realized they are more + // like integration tests because they test the whole logging process rather + // than individual functions. Im not sure where these should go. + + performRequest(router, "POST", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "POST") + assert.Contains(t, buffer.String(), "/example") + + performRequest(router, "PUT", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "PUT") + assert.Contains(t, buffer.String(), "/example") + + performRequest(router, "DELETE", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "DELETE") + assert.Contains(t, buffer.String(), "/example") + + performRequest(router, "PATCH", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "PATCH") + assert.Contains(t, buffer.String(), "/example") + + performRequest(router, "HEAD", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "HEAD") + assert.Contains(t, buffer.String(), "/example") + + performRequest(router, "OPTIONS", "/example") + assert.Contains(t, buffer.String(), "200") + assert.Contains(t, buffer.String(), "OPTIONS") + assert.Contains(t, buffer.String(), "/example") + + performRequest(router, "GET", "/notfound") + assert.Contains(t, buffer.String(), "404") + assert.Contains(t, buffer.String(), "GET") + assert.Contains(t, buffer.String(), "/notfound") + +} + +func TestColorForMethod(t *testing.T) { + assert.Equal(t, colorForMethod("GET"), string([]byte{27, 91, 57, 55, 59, 52, 52, 109}), "get should be blue") + assert.Equal(t, colorForMethod("POST"), string([]byte{27, 91, 57, 55, 59, 52, 54, 109}), "post should be cyan") + assert.Equal(t, colorForMethod("PUT"), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "put should be yellow") + assert.Equal(t, colorForMethod("DELETE"), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "delete should be red") + assert.Equal(t, colorForMethod("PATCH"), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "patch should be green") + assert.Equal(t, colorForMethod("HEAD"), string([]byte{27, 91, 57, 55, 59, 52, 53, 109}), "head should be magenta") + assert.Equal(t, colorForMethod("OPTIONS"), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "options should be white") + assert.Equal(t, colorForMethod("TRACE"), string([]byte{27, 91, 48, 109}), "trace is not defined and should be the reset color") +} + +func TestColorForStatus(t *testing.T) { + assert.Equal(t, colorForStatus(200), string([]byte{27, 91, 57, 55, 59, 52, 50, 109}), "2xx should be green") + assert.Equal(t, colorForStatus(301), string([]byte{27, 91, 57, 48, 59, 52, 55, 109}), "3xx should be white") + assert.Equal(t, colorForStatus(404), string([]byte{27, 91, 57, 55, 59, 52, 51, 109}), "4xx should be yellow") + assert.Equal(t, colorForStatus(2), string([]byte{27, 91, 57, 55, 59, 52, 49, 109}), "other things should be red") +} diff --git a/middleware_test.go b/middleware_test.go new file mode 100644 index 00000000..3a9d2c4e --- /dev/null +++ b/middleware_test.go @@ -0,0 +1,255 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "errors" + + "testing" + + "github.com/manucorporat/sse" + "github.com/stretchr/testify/assert" +) + +func TestMiddlewareGeneralCase(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + }) + router.GET("/", func(c *Context) { + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += " X " + }) + router.NoMethod(func(c *Context) { + signature += " XX " + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 200) + assert.Equal(t, signature, "ACDB") +} + +func TestMiddlewareNoRoute(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + c.Next() + c.Next() + c.Next() + c.Next() + signature += "D" + }) + router.NoRoute(func(c *Context) { + signature += "E" + c.Next() + signature += "F" + }, func(c *Context) { + signature += "G" + c.Next() + signature += "H" + }) + router.NoMethod(func(c *Context) { + signature += " X " + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 404) + assert.Equal(t, signature, "ACEGHFDB") +} + +func TestMiddlewareNoMethodEnabled(t *testing.T) { + signature := "" + router := New() + router.HandleMethodNotAllowed = true + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + c.Next() + signature += "D" + }) + router.NoMethod(func(c *Context) { + signature += "E" + c.Next() + signature += "F" + }, func(c *Context) { + signature += "G" + c.Next() + signature += "H" + }) + router.NoRoute(func(c *Context) { + signature += " X " + }) + router.POST("/", func(c *Context) { + signature += " XX " + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 405) + assert.Equal(t, signature, "ACEGHFDB") +} + +func TestMiddlewareNoMethodDisabled(t *testing.T) { + signature := "" + router := New() + router.HandleMethodNotAllowed = false + router.Use(func(c *Context) { + signature += "A" + c.Next() + signature += "B" + }) + router.Use(func(c *Context) { + signature += "C" + c.Next() + signature += "D" + }) + router.NoMethod(func(c *Context) { + signature += "E" + c.Next() + signature += "F" + }, func(c *Context) { + signature += "G" + c.Next() + signature += "H" + }) + router.NoRoute(func(c *Context) { + signature += " X " + }) + router.POST("/", func(c *Context) { + signature += " XX " + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 404) + assert.Equal(t, signature, "AC X DB") +} + +func TestMiddlewareAbort(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + }) + router.Use(func(c *Context) { + signature += "C" + c.AbortWithStatus(401) + c.Next() + signature += "D" + }) + router.GET("/", func(c *Context) { + signature += " X " + c.Next() + signature += " XX " + }) + + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 401) + assert.Equal(t, signature, "ACD") +} + +func TestMiddlewareAbortHandlersChainAndNext(t *testing.T) { + signature := "" + router := New() + router.Use(func(c *Context) { + signature += "A" + c.Next() + c.AbortWithStatus(410) + signature += "B" + + }) + router.GET("/", func(c *Context) { + signature += "C" + c.Next() + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 410) + assert.Equal(t, signature, "ACB") +} + +// TestFailHandlersChain - ensure that Fail interrupt used middleware in fifo order as +// as well as Abort +func TestMiddlewareFailHandlersChain(t *testing.T) { + // SETUP + signature := "" + router := New() + router.Use(func(context *Context) { + signature += "A" + context.AbortWithError(500, errors.New("foo")) + }) + router.Use(func(context *Context) { + signature += "B" + context.Next() + signature += "C" + }) + // RUN + w := performRequest(router, "GET", "/") + + // TEST + assert.Equal(t, w.Code, 500) + assert.Equal(t, signature, "A") +} + +func TestMiddlewareWrite(t *testing.T) { + router := New() + router.Use(func(c *Context) { + c.String(400, "hola\n") + }) + router.Use(func(c *Context) { + c.XML(400, H{"foo": "bar"}) + }) + router.Use(func(c *Context) { + c.JSON(400, H{"foo": "bar"}) + }) + router.GET("/", func(c *Context) { + c.JSON(400, H{"foo": "bar"}) + }, func(c *Context) { + c.Render(400, sse.Event{ + Event: "test", + Data: "message", + }) + }) + + w := performRequest(router, "GET", "/") + + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), `hola +bar{"foo":"bar"} +{"foo":"bar"} +event: test +data: message + +`) +} diff --git a/mode.go b/mode.go index 0495b830..15efaeb8 100644 --- a/mode.go +++ b/mode.go @@ -5,11 +5,14 @@ package gin import ( - "fmt" + "io" "os" + + "github.com/gin-gonic/gin/binding" + "github.com/mattn/go-colorable" ) -const GIN_MODE = "GIN_MODE" +const ENV_GIN_MODE = "GIN_MODE" const ( DebugMode string = "debug" @@ -22,42 +25,37 @@ const ( testCode = iota ) -var gin_mode int = debugCode -var mode_name string = DebugMode +var DefaultWriter io.Writer = colorable.NewColorableStdout() +var ginMode int = debugCode +var modeName string = DebugMode func init() { - value := os.Getenv(GIN_MODE) - if len(value) == 0 { + mode := os.Getenv(ENV_GIN_MODE) + if len(mode) == 0 { SetMode(DebugMode) } else { - SetMode(value) + SetMode(mode) } } func SetMode(value string) { switch value { case DebugMode: - gin_mode = debugCode + ginMode = debugCode case ReleaseMode: - gin_mode = releaseCode + ginMode = releaseCode case TestMode: - gin_mode = testCode + ginMode = testCode default: panic("gin mode unknown: " + value) } - mode_name = value + modeName = value +} + +func DisableBindValidation() { + binding.Validator = nil } func Mode() string { - return mode_name -} - -func IsDebugging() bool { - return gin_mode == debugCode -} - -func debugPrint(format string, values ...interface{}) { - if IsDebugging() { - fmt.Printf("[GIN-debug] "+format, values...) - } + return modeName } diff --git a/mode_test.go b/mode_test.go new file mode 100644 index 00000000..2a23d85e --- /dev/null +++ b/mode_test.go @@ -0,0 +1,31 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestSetMode(t *testing.T) { + SetMode(DebugMode) + assert.Equal(t, ginMode, debugCode) + assert.Equal(t, Mode(), DebugMode) + + SetMode(ReleaseMode) + assert.Equal(t, ginMode, releaseCode) + assert.Equal(t, Mode(), ReleaseMode) + + SetMode(TestMode) + assert.Equal(t, ginMode, testCode) + assert.Equal(t, Mode(), TestMode) + + assert.Panics(t, func() { SetMode("unknown") }) +} diff --git a/path.go b/path.go new file mode 100644 index 00000000..43cdd047 --- /dev/null +++ b/path.go @@ -0,0 +1,123 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +// CleanPath is the URL version of path.Clean, it returns a canonical URL path +// for p, eliminating . and .. elements. +// +// The following rules are applied iteratively until no further processing can +// be done: +// 1. Replace multiple slashes with a single slash. +// 2. Eliminate each . path name element (the current directory). +// 3. Eliminate each inner .. path name element (the parent directory) +// along with the non-.. element that precedes it. +// 4. Eliminate .. elements that begin a rooted path: +// that is, replace "/.." by "/" at the beginning of a path. +// +// If the result of this process is an empty string, "/" is returned +func cleanPath(p string) string { + // Turn empty string into "/" + if p == "" { + return "/" + } + + n := len(p) + var buf []byte + + // Invariants: + // reading from path; r is index of next byte to process. + // writing to buf; w is index of next byte to write. + + // path must start with '/' + r := 1 + w := 1 + + if p[0] != '/' { + r = 0 + buf = make([]byte, n+1) + buf[0] = '/' + } + + trailing := n > 2 && p[n-1] == '/' + + // A bit more clunky without a 'lazybuf' like the path package, but the loop + // gets completely inlined (bufApp). So in contrast to the path package this + // loop has no expensive function calls (except 1x make) + + for r < n { + switch { + case p[r] == '/': + // empty path element, trailing slash is added after the end + r++ + + case p[r] == '.' && r+1 == n: + trailing = true + r++ + + case p[r] == '.' && p[r+1] == '/': + // . element + r++ + + case p[r] == '.' && p[r+1] == '.' && (r+2 == n || p[r+2] == '/'): + // .. element: remove to last / + r += 2 + + if w > 1 { + // can backtrack + w-- + + if buf == nil { + for w > 1 && p[w] != '/' { + w-- + } + } else { + for w > 1 && buf[w] != '/' { + w-- + } + } + } + + default: + // real path element. + // add slash if needed + if w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + // copy element + for r < n && p[r] != '/' { + bufApp(&buf, p, w, p[r]) + w++ + r++ + } + } + } + + // re-append trailing slash + if trailing && w > 1 { + bufApp(&buf, p, w, '/') + w++ + } + + if buf == nil { + return p[:w] + } + return string(buf[:w]) +} + +// internal helper to lazily create a buffer if necessary +func bufApp(buf *[]byte, s string, w int, c byte) { + if *buf == nil { + if s[w] == c { + return + } + + *buf = make([]byte, len(s)) + copy(*buf, s[:w]) + } + (*buf)[w] = c +} diff --git a/path_test.go b/path_test.go new file mode 100644 index 00000000..01cb758a --- /dev/null +++ b/path_test.go @@ -0,0 +1,88 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Based on the path package, Copyright 2009 The Go Authors. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "runtime" + "testing" + + "github.com/stretchr/testify/assert" +) + +var cleanTests = []struct { + path, result string +}{ + // Already clean + {"/", "/"}, + {"/abc", "/abc"}, + {"/a/b/c", "/a/b/c"}, + {"/abc/", "/abc/"}, + {"/a/b/c/", "/a/b/c/"}, + + // missing root + {"", "/"}, + {"abc", "/abc"}, + {"abc/def", "/abc/def"}, + {"a/b/c", "/a/b/c"}, + + // Remove doubled slash + {"//", "/"}, + {"/abc//", "/abc/"}, + {"/abc/def//", "/abc/def/"}, + {"/a/b/c//", "/a/b/c/"}, + {"/abc//def//ghi", "/abc/def/ghi"}, + {"//abc", "/abc"}, + {"///abc", "/abc"}, + {"//abc//", "/abc/"}, + + // Remove . elements + {".", "/"}, + {"./", "/"}, + {"/abc/./def", "/abc/def"}, + {"/./abc/def", "/abc/def"}, + {"/abc/.", "/abc/"}, + + // Remove .. elements + {"..", "/"}, + {"../", "/"}, + {"../../", "/"}, + {"../..", "/"}, + {"../../abc", "/abc"}, + {"/abc/def/ghi/../jkl", "/abc/def/jkl"}, + {"/abc/def/../ghi/../jkl", "/abc/jkl"}, + {"/abc/def/..", "/abc"}, + {"/abc/def/../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../..", "/"}, + {"/abc/def/../../../ghi/jkl/../../../mno", "/mno"}, + + // Combinations + {"abc/./../def", "/def"}, + {"abc//./../def", "/def"}, + {"abc/../../././../def", "/def"}, +} + +func TestPathClean(t *testing.T) { + for _, test := range cleanTests { + assert.Equal(t, cleanPath(test.path), test.result) + assert.Equal(t, cleanPath(test.result), test.result) + } +} + +func TestPathCleanMallocs(t *testing.T) { + if testing.Short() { + t.Skip("skipping malloc count in short mode") + } + if runtime.GOMAXPROCS(0) > 1 { + t.Log("skipping AllocsPerRun checks; GOMAXPROCS>1") + return + } + + for _, test := range cleanTests { + allocs := testing.AllocsPerRun(100, func() { cleanPath(test.result) }) + assert.EqualValues(t, allocs, 0) + } +} diff --git a/recovery.go b/recovery.go index a8d537e4..e296e339 100644 --- a/recovery.go +++ b/recovery.go @@ -7,9 +7,9 @@ package gin import ( "bytes" "fmt" + "io" "io/ioutil" "log" - "net/http" "runtime" ) @@ -20,6 +20,30 @@ var ( slash = []byte("/") ) +// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. +func Recovery() HandlerFunc { + return RecoveryWithWriter(DefaultWriter) +} + +func RecoveryWithWriter(out io.Writer) HandlerFunc { + var logger *log.Logger + if out != nil { + logger = log.New(out, "", log.LstdFlags) + } + return func(c *Context) { + defer func() { + if err := recover(); err != nil { + if logger != nil { + stack := stack(3) + logger.Printf("Panic recovery -> %s\n%s\n", err, stack) + } + c.AbortWithStatus(500) + } + }() + c.Next() + } +} + // stack returns a nicely formated stack frame, skipping skip frames func stack(skip int) []byte { buf := new(bytes.Buffer) // the returned data @@ -80,19 +104,3 @@ func function(pc uintptr) []byte { name = bytes.Replace(name, centerDot, dot, -1) return name } - -// Recovery returns a middleware that recovers from any panics and writes a 500 if there was one. -// While Martini is in development mode, Recovery will also output the panic as HTML. -func Recovery() HandlerFunc { - return func(c *Context) { - defer func() { - if err := recover(); err != nil { - stack := stack(3) - log.Printf("PANIC: %s\n%s", err, stack) - c.Writer.WriteHeader(http.StatusInternalServerError) - } - }() - - c.Next() - } -} diff --git a/recovery_test.go b/recovery_test.go index f9047e24..39e71e81 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -6,51 +6,37 @@ package gin import ( "bytes" - "log" - "os" "testing" + + "github.com/stretchr/testify/assert" ) // TestPanicInHandler assert that panic has been recovered. func TestPanicInHandler(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) // Disable panic logs for testing - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(_ *Context) { + buffer := new(bytes.Buffer) + router := New() + router.Use(RecoveryWithWriter(buffer)) + router.GET("/recovery", func(_ *Context) { panic("Oupps, Houston, we have a problem") }) - // RUN - w := PerformRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - - if w.Code != 500 { - t.Errorf("Response code should be Internal Server Error, was: %s", w.Code) - } + w := performRequest(router, "GET", "/recovery") + // TEST + assert.Equal(t, w.Code, 500) + assert.Contains(t, buffer.String(), "Panic recovery -> Oupps, Houston, we have a problem") + assert.Contains(t, buffer.String(), "TestPanicInHandler") } // TestPanicWithAbort assert that panic has been recovered even if context.Abort was used. func TestPanicWithAbort(t *testing.T) { - // SETUP - log.SetOutput(bytes.NewBuffer(nil)) - r := New() - r.Use(Recovery()) - r.GET("/recovery", func(c *Context) { + router := New() + router.Use(RecoveryWithWriter(nil)) + router.GET("/recovery", func(c *Context) { c.AbortWithStatus(400) panic("Oupps, Houston, we have a problem") }) - // RUN - w := PerformRequest(r, "GET", "/recovery") - - // restore logging - log.SetOutput(os.Stderr) - + w := performRequest(router, "GET", "/recovery") // TEST - if w.Code != 500 { - t.Errorf("Response code should be Bad request, was: %s", w.Code) - } + assert.Equal(t, w.Code, 500) // NOT SURE } diff --git a/render/data.go b/render/data.go new file mode 100644 index 00000000..efa75d55 --- /dev/null +++ b/render/data.go @@ -0,0 +1,20 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import "net/http" + +type Data struct { + ContentType string + Data []byte +} + +func (r Data) Render(w http.ResponseWriter) error { + if len(r.ContentType) > 0 { + w.Header()["Content-Type"] = []string{r.ContentType} + } + w.Write(r.Data) + return nil +} diff --git a/render/html.go b/render/html.go new file mode 100644 index 00000000..01f6bf22 --- /dev/null +++ b/render/html.go @@ -0,0 +1,67 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "html/template" + "net/http" +) + +type ( + HTMLRender interface { + Instance(string, interface{}) Render + } + + HTMLProduction struct { + Template *template.Template + } + + HTMLDebug struct { + Files []string + Glob string + } + + HTML struct { + Template *template.Template + Name string + Data interface{} + } +) + +var htmlContentType = []string{"text/html; charset=utf-8"} + +func (r HTMLProduction) Instance(name string, data interface{}) Render { + return HTML{ + Template: r.Template, + Name: name, + Data: data, + } +} + +func (r HTMLDebug) Instance(name string, data interface{}) Render { + return HTML{ + Template: r.loadTemplate(), + Name: name, + Data: data, + } +} +func (r HTMLDebug) loadTemplate() *template.Template { + if len(r.Files) > 0 { + return template.Must(template.ParseFiles(r.Files...)) + } + if len(r.Glob) > 0 { + return template.Must(template.ParseGlob(r.Glob)) + } + panic("the HTML debug render was created without files or glob pattern") +} + +func (r HTML) Render(w http.ResponseWriter) error { + writeContentType(w, htmlContentType) + if len(r.Name) == 0 { + return r.Template.Execute(w, r.Data) + } else { + return r.Template.ExecuteTemplate(w, r.Name, r.Data) + } +} diff --git a/render/json.go b/render/json.go new file mode 100644 index 00000000..32e6058d --- /dev/null +++ b/render/json.go @@ -0,0 +1,41 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "encoding/json" + "net/http" +) + +type ( + JSON struct { + Data interface{} + } + + IndentedJSON struct { + Data interface{} + } +) + +var jsonContentType = []string{"application/json; charset=utf-8"} + +func (r JSON) Render(w http.ResponseWriter) error { + return WriteJSON(w, r.Data) +} + +func (r IndentedJSON) Render(w http.ResponseWriter) error { + writeContentType(w, jsonContentType) + jsonBytes, err := json.MarshalIndent(r.Data, "", " ") + if err != nil { + return err + } + w.Write(jsonBytes) + return nil +} + +func WriteJSON(w http.ResponseWriter, obj interface{}) error { + writeContentType(w, jsonContentType) + return json.NewEncoder(w).Encode(obj) +} diff --git a/render/redirect.go b/render/redirect.go new file mode 100644 index 00000000..d64e4d75 --- /dev/null +++ b/render/redirect.go @@ -0,0 +1,24 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "fmt" + "net/http" +) + +type Redirect struct { + Code int + Request *http.Request + Location string +} + +func (r Redirect) Render(w http.ResponseWriter) error { + if r.Code < 300 || r.Code > 308 { + panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code)) + } + http.Redirect(w, r.Request, r.Location, r.Code) + return nil +} diff --git a/render/render.go b/render/render.go index a81fffe9..994fcd7c 100644 --- a/render/render.go +++ b/render/render.go @@ -4,120 +4,27 @@ package render -import ( - "encoding/json" - "encoding/xml" - "fmt" - "html/template" - "net/http" -) +import "net/http" -type ( - Render interface { - Render(http.ResponseWriter, int, ...interface{}) error - } - - // JSON binding - jsonRender struct{} - - // XML binding - xmlRender struct{} - - // Plain text - plainRender struct{} - - // Redirects - redirectRender struct{} - - // Redirects - htmlDebugRender struct { - files []string - globs []string - } - - // form binding - HTMLRender struct { - Template *template.Template - } -) +type Render interface { + Render(http.ResponseWriter) error +} var ( - JSON = jsonRender{} - XML = xmlRender{} - Plain = plainRender{} - Redirect = redirectRender{} - HTMLDebug = &htmlDebugRender{} + _ Render = JSON{} + _ Render = IndentedJSON{} + _ Render = XML{} + _ Render = String{} + _ Render = Redirect{} + _ Render = Data{} + _ Render = HTML{} + _ HTMLRender = HTMLDebug{} + _ HTMLRender = HTMLProduction{} ) -func writeHeader(w http.ResponseWriter, code int, contentType string) { - w.Header().Set("Content-Type", contentType) - w.WriteHeader(code) -} - -func (_ jsonRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "application/json") - encoder := json.NewEncoder(w) - return encoder.Encode(data[0]) -} - -func (_ redirectRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - w.Header().Set("Location", data[0].(string)) - w.WriteHeader(code) - return nil -} - -func (_ xmlRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "application/xml") - encoder := xml.NewEncoder(w) - return encoder.Encode(data[0]) -} - -func (_ plainRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/plain") - format := data[0].(string) - args := data[1].([]interface{}) - var err error - if len(args) > 0 { - _, err = w.Write([]byte(fmt.Sprintf(format, args...))) - } else { - _, err = w.Write([]byte(format)) +func writeContentType(w http.ResponseWriter, value []string) { + header := w.Header() + if val := header["Content-Type"]; len(val) == 0 { + header["Content-Type"] = value } - return err -} - -func (r *htmlDebugRender) AddGlob(pattern string) { - r.globs = append(r.globs, pattern) -} - -func (r *htmlDebugRender) AddFiles(files ...string) { - r.files = append(r.files, files...) -} - -func (r *htmlDebugRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") - file := data[0].(string) - obj := data[1] - - t := template.New("") - - if len(r.files) > 0 { - if _, err := t.ParseFiles(r.files...); err != nil { - return err - } - } - - for _, glob := range r.globs { - if _, err := t.ParseGlob(glob); err != nil { - return err - } - } - - return t.ExecuteTemplate(w, file, obj) -} - -func (html HTMLRender) Render(w http.ResponseWriter, code int, data ...interface{}) error { - writeHeader(w, code, "text/html") - file := data[0].(string) - obj := data[1] - return html.Template.ExecuteTemplate(w, file, obj) } diff --git a/render/render_test.go b/render/render_test.go new file mode 100644 index 00000000..7a6ffb7d --- /dev/null +++ b/render/render_test.go @@ -0,0 +1,130 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "encoding/xml" + "html/template" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TODO unit tests +// test errors + +func TestRenderJSON(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]interface{}{ + "foo": "bar", + } + + err := (JSON{data}).Render(w) + + assert.NoError(t, err) + assert.Equal(t, w.Body.String(), "{\"foo\":\"bar\"}\n") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +func TestRenderIndentedJSON(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]interface{}{ + "foo": "bar", + "bar": "foo", + } + + err := (IndentedJSON{data}).Render(w) + + assert.NoError(t, err) + assert.Equal(t, w.Body.String(), "{\n \"bar\": \"foo\",\n \"foo\": \"bar\"\n}") + assert.Equal(t, w.Header().Get("Content-Type"), "application/json; charset=utf-8") +} + +type xmlmap map[string]interface{} + +// Allows type H to be used with xml.Marshal +func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error { + start.Name = xml.Name{ + Space: "", + Local: "map", + } + if err := e.EncodeToken(start); err != nil { + return err + } + for key, value := range h { + elem := xml.StartElement{ + Name: xml.Name{Space: "", Local: key}, + Attr: []xml.Attr{}, + } + if err := e.EncodeElement(value, elem); err != nil { + return err + } + } + if err := e.EncodeToken(xml.EndElement{Name: start.Name}); err != nil { + return err + } + return nil +} + +func TestRenderXML(t *testing.T) { + w := httptest.NewRecorder() + data := xmlmap{ + "foo": "bar", + } + + err := (XML{data}).Render(w) + + assert.NoError(t, err) + assert.Equal(t, w.Body.String(), "bar") + assert.Equal(t, w.Header().Get("Content-Type"), "application/xml; charset=utf-8") +} + +func TestRenderRedirect(t *testing.T) { + // TODO +} + +func TestRenderData(t *testing.T) { + w := httptest.NewRecorder() + data := []byte("#!PNG some raw data") + + err := (Data{ + ContentType: "image/png", + Data: data, + }).Render(w) + + assert.NoError(t, err) + assert.Equal(t, w.Body.String(), "#!PNG some raw data") + assert.Equal(t, w.Header().Get("Content-Type"), "image/png") +} + +func TestRenderString(t *testing.T) { + w := httptest.NewRecorder() + + err := (String{ + Format: "hola %s %d", + Data: []interface{}{"manu", 2}, + }).Render(w) + + assert.NoError(t, err) + assert.Equal(t, w.Body.String(), "hola manu 2") + assert.Equal(t, w.Header().Get("Content-Type"), "text/plain; charset=utf-8") +} + +func TestRenderHTMLTemplate(t *testing.T) { + w := httptest.NewRecorder() + templ := template.Must(template.New("t").Parse(`Hello {{.name}}`)) + + htmlRender := HTMLProduction{Template: templ} + instance := htmlRender.Instance("t", map[string]interface{}{ + "name": "alexandernyquist", + }) + + err := instance.Render(w) + + assert.NoError(t, err) + assert.Equal(t, w.Body.String(), "Hello alexandernyquist") + assert.Equal(t, w.Header().Get("Content-Type"), "text/html; charset=utf-8") +} diff --git a/render/text.go b/render/text.go new file mode 100644 index 00000000..5a9e280b --- /dev/null +++ b/render/text.go @@ -0,0 +1,33 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "fmt" + "io" + "net/http" +) + +type String struct { + Format string + Data []interface{} +} + +var plainContentType = []string{"text/plain; charset=utf-8"} + +func (r String) Render(w http.ResponseWriter) error { + WriteString(w, r.Format, r.Data) + return nil +} + +func WriteString(w http.ResponseWriter, format string, data []interface{}) { + writeContentType(w, plainContentType) + + if len(data) > 0 { + fmt.Fprintf(w, format, data...) + } else { + io.WriteString(w, format) + } +} diff --git a/render/xml.go b/render/xml.go new file mode 100644 index 00000000..be22e6f2 --- /dev/null +++ b/render/xml.go @@ -0,0 +1,21 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package render + +import ( + "encoding/xml" + "net/http" +) + +type XML struct { + Data interface{} +} + +var xmlContentType = []string{"application/xml; charset=utf-8"} + +func (r XML) Render(w http.ResponseWriter) error { + writeContentType(w, xmlContentType) + return xml.NewEncoder(w).Encode(r.Data) +} diff --git a/response_writer.go b/response_writer.go index 98993958..5a75335f 100644 --- a/response_writer.go +++ b/response_writer.go @@ -6,14 +6,14 @@ package gin import ( "bufio" - "errors" - "log" + "io" "net" "net/http" ) const ( - NoWritten = -1 + noWritten = -1 + defaultStatus = 200 ) type ( @@ -25,29 +25,32 @@ type ( Status() int Size() int + WriteString(string) (int, error) Written() bool WriteHeaderNow() } responseWriter struct { http.ResponseWriter - status int size int + status int } ) +var _ ResponseWriter = &responseWriter{} + func (w *responseWriter) reset(writer http.ResponseWriter) { w.ResponseWriter = writer - w.status = 200 - w.size = NoWritten + w.size = noWritten + w.status = defaultStatus } func (w *responseWriter) WriteHeader(code int) { - if code > 0 { - w.status = code + if code > 0 && w.status != code { if w.Written() { - log.Println("[GIN] WARNING. Headers were already written!") + debugPrint("[WARNING] Headers were already written. Wanted to override status code %d with %d", w.status, code) } + w.status = code } } @@ -65,6 +68,13 @@ func (w *responseWriter) Write(data []byte) (n int, err error) { return } +func (w *responseWriter) WriteString(s string) (n int, err error) { + w.WriteHeaderNow() + n, err = io.WriteString(w.ResponseWriter, s) + w.size += n + return +} + func (w *responseWriter) Status() int { return w.status } @@ -74,16 +84,15 @@ func (w *responseWriter) Size() int { } func (w *responseWriter) Written() bool { - return w.size != NoWritten + return w.size != noWritten } // Implements the http.Hijacker interface func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { - hijacker, ok := w.ResponseWriter.(http.Hijacker) - if !ok { - return nil, nil, errors.New("the ResponseWriter doesn't support the Hijacker interface") + if w.size < 0 { + w.size = 0 } - return hijacker.Hijack() + return w.ResponseWriter.(http.Hijacker).Hijack() } // Implements the http.CloseNotify interface @@ -93,8 +102,5 @@ func (w *responseWriter) CloseNotify() <-chan bool { // Implements the http.Flush interface func (w *responseWriter) Flush() { - flusher, ok := w.ResponseWriter.(http.Flusher) - if ok { - flusher.Flush() - } + w.ResponseWriter.(http.Flusher).Flush() } diff --git a/response_writer_test.go b/response_writer_test.go new file mode 100644 index 00000000..7306d192 --- /dev/null +++ b/response_writer_test.go @@ -0,0 +1,115 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +// TODO +// func (w *responseWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) { +// func (w *responseWriter) CloseNotify() <-chan bool { +// func (w *responseWriter) Flush() { + +var _ ResponseWriter = &responseWriter{} +var _ http.ResponseWriter = &responseWriter{} +var _ http.ResponseWriter = ResponseWriter(&responseWriter{}) +var _ http.Hijacker = ResponseWriter(&responseWriter{}) +var _ http.Flusher = ResponseWriter(&responseWriter{}) +var _ http.CloseNotifier = ResponseWriter(&responseWriter{}) + +func init() { + SetMode(TestMode) +} + +func TestResponseWriterReset(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + var w ResponseWriter = writer + + writer.reset(testWritter) + assert.Equal(t, writer.size, -1) + assert.Equal(t, writer.status, 200) + assert.Equal(t, writer.ResponseWriter, testWritter) + assert.Equal(t, w.Size(), -1) + assert.Equal(t, w.Status(), 200) + assert.False(t, w.Written()) +} + +func TestResponseWriterWriteHeader(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + w.WriteHeader(300) + assert.False(t, w.Written()) + assert.Equal(t, w.Status(), 300) + assert.NotEqual(t, testWritter.Code, 300) + + w.WriteHeader(-1) + assert.Equal(t, w.Status(), 300) +} + +func TestResponseWriterWriteHeadersNow(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + w.WriteHeader(300) + w.WriteHeaderNow() + + assert.True(t, w.Written()) + assert.Equal(t, w.Size(), 0) + assert.Equal(t, testWritter.Code, 300) + + writer.size = 10 + w.WriteHeaderNow() + assert.Equal(t, w.Size(), 10) +} + +func TestResponseWriterWrite(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + n, err := w.Write([]byte("hola")) + assert.Equal(t, n, 4) + assert.Equal(t, w.Size(), 4) + assert.Equal(t, w.Status(), 200) + assert.Equal(t, testWritter.Code, 200) + assert.Equal(t, testWritter.Body.String(), "hola") + assert.NoError(t, err) + + n, err = w.Write([]byte(" adios")) + assert.Equal(t, n, 6) + assert.Equal(t, w.Size(), 10) + assert.Equal(t, testWritter.Body.String(), "hola adios") + assert.NoError(t, err) +} + +func TestResponseWriterHijack(t *testing.T) { + testWritter := httptest.NewRecorder() + writer := &responseWriter{} + writer.reset(testWritter) + w := ResponseWriter(writer) + + assert.Panics(t, func() { + w.Hijack() + }) + assert.True(t, w.Written()) + + assert.Panics(t, func() { + w.CloseNotify() + }) + + w.Flush() +} diff --git a/routergroup.go b/routergroup.go index 8b2ebdd2..90fbb404 100644 --- a/routergroup.go +++ b/routergroup.go @@ -5,36 +5,57 @@ package gin import ( - "github.com/julienschmidt/httprouter" "net/http" "path" + "regexp" + "strings" ) -// Used internally to configure router, a RouterGroup is associated with a prefix -// and an array of handlers (middlewares) -type RouterGroup struct { - Handlers []HandlerFunc - absolutePath string - engine *Engine +type routesInterface interface { + Use(...HandlerFunc) routesInterface + + Handle(string, string, ...HandlerFunc) routesInterface + Any(string, ...HandlerFunc) routesInterface + GET(string, ...HandlerFunc) routesInterface + POST(string, ...HandlerFunc) routesInterface + DELETE(string, ...HandlerFunc) routesInterface + PATCH(string, ...HandlerFunc) routesInterface + PUT(string, ...HandlerFunc) routesInterface + OPTIONS(string, ...HandlerFunc) routesInterface + HEAD(string, ...HandlerFunc) routesInterface + + StaticFile(string, string) routesInterface + Static(string, string) routesInterface + StaticFS(string, http.FileSystem) routesInterface } -// Adds middlewares to the group, see example code in github. -func (group *RouterGroup) Use(middlewares ...HandlerFunc) { - group.Handlers = append(group.Handlers, middlewares...) +// Used internally to configure router, a RouterGroup is associated with a prefix +// and an array of handlers (middleware) +type RouterGroup struct { + Handlers HandlersChain + BasePath string + engine *Engine + root bool +} + +// Adds middleware to the group, see example code in github. +func (group *RouterGroup) Use(middleware ...HandlerFunc) routesInterface { + group.Handlers = append(group.Handlers, middleware...) + return group.returnObj() } // Creates a new router group. You should add all the routes that have common middlwares or the same path prefix. // For example, all the routes that use a common middlware for authorization could be grouped. func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *RouterGroup { return &RouterGroup{ - Handlers: group.combineHandlers(handlers), - absolutePath: group.calculateAbsolutePath(relativePath), - engine: group.engine, + Handlers: group.combineHandlers(handlers), + BasePath: group.calculateAbsolutePath(relativePath), + engine: group.engine, } } -// Handle registers a new request handle and middlewares with the given path and method. -// The last handler should be the real handler, the other ones should be middlewares that can and should be shared among different routes. +// Handle registers a new request handle and middleware with the given path and method. +// The last handler should be the real handler, the other ones should be middleware that can and should be shared among different routes. // See the example code in github. // // For GET, POST, PUT, PATCH and DELETE requests the respective shortcut @@ -43,56 +64,79 @@ func (group *RouterGroup) Group(relativePath string, handlers ...HandlerFunc) *R // This function is intended for bulk loading and to allow the usage of less // frequently used, non-standardized or custom methods (e.g. for internal // communication with a proxy). -func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers []HandlerFunc) { +func (group *RouterGroup) handle(httpMethod, relativePath string, handlers HandlersChain) routesInterface { absolutePath := group.calculateAbsolutePath(relativePath) handlers = group.combineHandlers(handlers) - if IsDebugging() { - nuHandlers := len(handlers) - handlerName := nameOfFunction(handlers[nuHandlers-1]) - debugPrint("%-5s %-25s --> %s (%d handlers)\n", httpMethod, absolutePath, handlerName, nuHandlers) - } + group.engine.addRoute(httpMethod, absolutePath, handlers) + return group.returnObj() +} - group.engine.router.Handle(httpMethod, absolutePath, func(w http.ResponseWriter, req *http.Request, params httprouter.Params) { - context := group.engine.createContext(w, req, params, handlers) - context.Next() - context.Writer.WriteHeaderNow() - group.engine.reuseContext(context) - }) +func (group *RouterGroup) Handle(httpMethod, relativePath string, handlers ...HandlerFunc) routesInterface { + if matches, err := regexp.MatchString("^[A-Z]+$", httpMethod); !matches || err != nil { + panic("http method " + httpMethod + " is not valid") + } + return group.handle(httpMethod, relativePath, handlers) } // POST is a shortcut for router.Handle("POST", path, handle) -func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) { - group.Handle("POST", relativePath, handlers) +func (group *RouterGroup) POST(relativePath string, handlers ...HandlerFunc) routesInterface { + return group.handle("POST", relativePath, handlers) } // GET is a shortcut for router.Handle("GET", path, handle) -func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) { - group.Handle("GET", relativePath, handlers) +func (group *RouterGroup) GET(relativePath string, handlers ...HandlerFunc) routesInterface { + return group.handle("GET", relativePath, handlers) } // DELETE is a shortcut for router.Handle("DELETE", path, handle) -func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) { - group.Handle("DELETE", relativePath, handlers) +func (group *RouterGroup) DELETE(relativePath string, handlers ...HandlerFunc) routesInterface { + return group.handle("DELETE", relativePath, handlers) } // PATCH is a shortcut for router.Handle("PATCH", path, handle) -func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) { - group.Handle("PATCH", relativePath, handlers) +func (group *RouterGroup) PATCH(relativePath string, handlers ...HandlerFunc) routesInterface { + return group.handle("PATCH", relativePath, handlers) } // PUT is a shortcut for router.Handle("PUT", path, handle) -func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) { - group.Handle("PUT", relativePath, handlers) +func (group *RouterGroup) PUT(relativePath string, handlers ...HandlerFunc) routesInterface { + return group.handle("PUT", relativePath, handlers) } // OPTIONS is a shortcut for router.Handle("OPTIONS", path, handle) -func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) { - group.Handle("OPTIONS", relativePath, handlers) +func (group *RouterGroup) OPTIONS(relativePath string, handlers ...HandlerFunc) routesInterface { + return group.handle("OPTIONS", relativePath, handlers) } // HEAD is a shortcut for router.Handle("HEAD", path, handle) -func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) { - group.Handle("HEAD", relativePath, handlers) +func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) routesInterface { + return group.handle("HEAD", relativePath, handlers) +} + +func (group *RouterGroup) Any(relativePath string, handlers ...HandlerFunc) routesInterface { + // GET, POST, PUT, PATCH, HEAD, OPTIONS, DELETE, CONNECT, TRACE + group.handle("GET", relativePath, handlers) + group.handle("POST", relativePath, handlers) + group.handle("PUT", relativePath, handlers) + group.handle("PATCH", relativePath, handlers) + group.handle("HEAD", relativePath, handlers) + group.handle("OPTIONS", relativePath, handlers) + group.handle("DELETE", relativePath, handlers) + group.handle("CONNECT", relativePath, handlers) + group.handle("TRACE", relativePath, handlers) + return group.returnObj() +} + +func (group *RouterGroup) StaticFile(relativePath, filepath string) routesInterface { + if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { + panic("URL parameters can not be used when serving a static file") + } + handler := func(c *Context) { + c.File(filepath) + } + group.GET(relativePath, handler) + group.HEAD(relativePath, handler) + return group.returnObj() } // Static serves files from the given file system root. @@ -101,38 +145,54 @@ func (group *RouterGroup) HEAD(relativePath string, handlers ...HandlerFunc) { // To use the operating system's file system implementation, // use : // router.Static("/static", "/var/www") -func (group *RouterGroup) Static(relativePath, root string) { - absolutePath := group.calculateAbsolutePath(relativePath) - handler := group.createStaticHandler(absolutePath, root) - absolutePath = path.Join(absolutePath, "/*filepath") - - // Register GET and HEAD handlers - group.GET(absolutePath, handler) - group.HEAD(absolutePath, handler) +func (group *RouterGroup) Static(relativePath, root string) routesInterface { + return group.StaticFS(relativePath, Dir(root, false)) } -func (group *RouterGroup) createStaticHandler(absolutePath, root string) func(*Context) { - fileServer := http.StripPrefix(absolutePath, http.FileServer(http.Dir(root))) +func (group *RouterGroup) StaticFS(relativePath string, fs http.FileSystem) routesInterface { + if strings.Contains(relativePath, ":") || strings.Contains(relativePath, "*") { + panic("URL parameters can not be used when serving a static folder") + } + handler := group.createStaticHandler(relativePath, fs) + urlPattern := path.Join(relativePath, "/*filepath") + + // Register GET and HEAD handlers + group.GET(urlPattern, handler) + group.HEAD(urlPattern, handler) + return group.returnObj() +} + +func (group *RouterGroup) createStaticHandler(relativePath string, fs http.FileSystem) HandlerFunc { + absolutePath := group.calculateAbsolutePath(relativePath) + fileServer := http.StripPrefix(absolutePath, http.FileServer(fs)) + _, nolisting := fs.(*onlyfilesFS) return func(c *Context) { + if nolisting { + c.Writer.WriteHeader(404) + } fileServer.ServeHTTP(c.Writer, c.Request) } } -func (group *RouterGroup) combineHandlers(handlers []HandlerFunc) []HandlerFunc { +func (group *RouterGroup) combineHandlers(handlers HandlersChain) HandlersChain { finalSize := len(group.Handlers) + len(handlers) - mergedHandlers := make([]HandlerFunc, 0, finalSize) - mergedHandlers = append(mergedHandlers, group.Handlers...) - return append(mergedHandlers, handlers...) + if finalSize >= int(AbortIndex) { + panic("too many handlers") + } + mergedHandlers := make(HandlersChain, finalSize) + copy(mergedHandlers, group.Handlers) + copy(mergedHandlers[len(group.Handlers):], handlers) + return mergedHandlers } func (group *RouterGroup) calculateAbsolutePath(relativePath string) string { - if len(relativePath) == 0 { - return group.absolutePath - } - absolutePath := path.Join(group.absolutePath, relativePath) - appendSlash := lastChar(relativePath) == '/' && lastChar(absolutePath) != '/' - if appendSlash { - return absolutePath + "/" - } - return absolutePath + return joinPaths(group.BasePath, relativePath) +} + +func (group *RouterGroup) returnObj() routesInterface { + if group.root { + return group.engine + } else { + return group + } } diff --git a/routergroup_test.go b/routergroup_test.go new file mode 100644 index 00000000..14c7421b --- /dev/null +++ b/routergroup_test.go @@ -0,0 +1,177 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +func TestRouterGroupBasic(t *testing.T) { + router := New() + group := router.Group("/hola", func(c *Context) {}) + group.Use(func(c *Context) {}) + + assert.Len(t, group.Handlers, 2) + assert.Equal(t, group.BasePath, "/hola") + assert.Equal(t, group.engine, router) + + group2 := group.Group("manu") + group2.Use(func(c *Context) {}, func(c *Context) {}) + + assert.Len(t, group2.Handlers, 4) + assert.Equal(t, group2.BasePath, "/hola/manu") + assert.Equal(t, group2.engine, router) +} + +func TestRouterGroupBasicHandle(t *testing.T) { + performRequestInGroup(t, "GET") + performRequestInGroup(t, "POST") + performRequestInGroup(t, "PUT") + performRequestInGroup(t, "PATCH") + performRequestInGroup(t, "DELETE") + performRequestInGroup(t, "HEAD") + performRequestInGroup(t, "OPTIONS") +} + +func performRequestInGroup(t *testing.T, method string) { + router := New() + v1 := router.Group("v1", func(c *Context) {}) + assert.Equal(t, v1.BasePath, "/v1") + + login := v1.Group("/login/", func(c *Context) {}, func(c *Context) {}) + assert.Equal(t, login.BasePath, "/v1/login/") + + handler := func(c *Context) { + c.String(400, "the method was %s and index %d", c.Request.Method, c.index) + } + + switch method { + case "GET": + v1.GET("/test", handler) + login.GET("/test", handler) + case "POST": + v1.POST("/test", handler) + login.POST("/test", handler) + case "PUT": + v1.PUT("/test", handler) + login.PUT("/test", handler) + case "PATCH": + v1.PATCH("/test", handler) + login.PATCH("/test", handler) + case "DELETE": + v1.DELETE("/test", handler) + login.DELETE("/test", handler) + case "HEAD": + v1.HEAD("/test", handler) + login.HEAD("/test", handler) + case "OPTIONS": + v1.OPTIONS("/test", handler) + login.OPTIONS("/test", handler) + default: + panic("unknown method") + } + + w := performRequest(router, method, "/v1/login/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 3") + + w = performRequest(router, method, "/v1/test") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "the method was "+method+" and index 1") +} + +func TestRouterGroupInvalidStatic(t *testing.T) { + router := New() + assert.Panics(t, func() { + router.Static("/path/:param", "/") + }) + + assert.Panics(t, func() { + router.Static("/path/*param", "/") + }) +} + +func TestRouterGroupInvalidStaticFile(t *testing.T) { + router := New() + assert.Panics(t, func() { + router.StaticFile("/path/:param", "favicon.ico") + }) + + assert.Panics(t, func() { + router.StaticFile("/path/*param", "favicon.ico") + }) +} + +func TestRouterGroupTooManyHandlers(t *testing.T) { + router := New() + handlers1 := make([]HandlerFunc, 40) + router.Use(handlers1...) + + handlers2 := make([]HandlerFunc, 26) + assert.Panics(t, func() { + router.Use(handlers2...) + }) + assert.Panics(t, func() { + router.GET("/", handlers2...) + }) +} + +func TestRouterGroupBadMethod(t *testing.T) { + router := New() + assert.Panics(t, func() { + router.Handle("get", "/") + }) + assert.Panics(t, func() { + router.Handle(" GET", "/") + }) + assert.Panics(t, func() { + router.Handle("GET ", "/") + }) + assert.Panics(t, func() { + router.Handle("", "/") + }) + assert.Panics(t, func() { + router.Handle("PO ST", "/") + }) + assert.Panics(t, func() { + router.Handle("1GET", "/") + }) + assert.Panics(t, func() { + router.Handle("PATCh", "/") + }) +} + +func TestRouterGroupPipeline(t *testing.T) { + router := New() + testRoutesInterface(t, router) + + v1 := router.Group("/v1") + testRoutesInterface(t, v1) +} + +func testRoutesInterface(t *testing.T, r routesInterface) { + handler := func(c *Context) {} + assert.Equal(t, r, r.Use(handler)) + + assert.Equal(t, r, r.Handle("GET", "/handler", handler)) + assert.Equal(t, r, r.Any("/any", handler)) + assert.Equal(t, r, r.GET("/", handler)) + assert.Equal(t, r, r.POST("/", handler)) + assert.Equal(t, r, r.DELETE("/", handler)) + assert.Equal(t, r, r.PATCH("/", handler)) + assert.Equal(t, r, r.PUT("/", handler)) + assert.Equal(t, r, r.OPTIONS("/", handler)) + assert.Equal(t, r, r.HEAD("/", handler)) + + assert.Equal(t, r, r.StaticFile("/file", ".")) + assert.Equal(t, r, r.Static("/static", ".")) + assert.Equal(t, r, r.StaticFS("/static2", Dir(".", false))) +} diff --git a/routes_test.go b/routes_test.go new file mode 100644 index 00000000..2f451f84 --- /dev/null +++ b/routes_test.go @@ -0,0 +1,329 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func performRequest(r http.Handler, method, path string) *httptest.ResponseRecorder { + req, _ := http.NewRequest(method, path, nil) + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +func testRouteOK(method string, t *testing.T) { + passed := false + passedAny := false + r := New() + r.Any("/test2", func(c *Context) { + passedAny = true + }) + r.Handle(method, "/test", func(c *Context) { + passed = true + }) + + w := performRequest(r, method, "/test") + assert.True(t, passed) + assert.Equal(t, w.Code, http.StatusOK) + + performRequest(r, method, "/test2") + assert.True(t, passedAny) + +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func testRouteNotOK(method string, t *testing.T) { + passed := false + router := New() + router.Handle(method, "/test_2", func(c *Context) { + passed = true + }) + + w := performRequest(router, method, "/test") + + assert.False(t, passed) + assert.Equal(t, w.Code, http.StatusNotFound) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func testRouteNotOK2(method string, t *testing.T) { + passed := false + router := New() + router.HandleMethodNotAllowed = true + var methodRoute string + if method == "POST" { + methodRoute = "GET" + } else { + methodRoute = "POST" + } + router.Handle(methodRoute, "/test", func(c *Context) { + passed = true + }) + + w := performRequest(router, method, "/test") + + assert.False(t, passed) + assert.Equal(t, w.Code, http.StatusMethodNotAllowed) +} + +func TestRouterMethod(t *testing.T) { + router := New() + router.PUT("/hey2", func(c *Context) { + c.String(200, "sup2") + }) + + router.PUT("/hey", func(c *Context) { + c.String(200, "called") + }) + + router.PUT("/hey3", func(c *Context) { + c.String(200, "sup3") + }) + + w := performRequest(router, "PUT", "/hey") + + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "called") +} + +func TestRouterGroupRouteOK(t *testing.T) { + testRouteOK("GET", t) + testRouteOK("POST", t) + testRouteOK("PUT", t) + testRouteOK("PATCH", t) + testRouteOK("HEAD", t) + testRouteOK("OPTIONS", t) + testRouteOK("DELETE", t) + testRouteOK("CONNECT", t) + testRouteOK("TRACE", t) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func TestRouteNotOK(t *testing.T) { + testRouteNotOK("GET", t) + testRouteNotOK("POST", t) + testRouteNotOK("PUT", t) + testRouteNotOK("PATCH", t) + testRouteNotOK("HEAD", t) + testRouteNotOK("OPTIONS", t) + testRouteNotOK("DELETE", t) + testRouteNotOK("CONNECT", t) + testRouteNotOK("TRACE", t) +} + +// TestSingleRouteOK tests that POST route is correctly invoked. +func TestRouteNotOK2(t *testing.T) { + testRouteNotOK2("GET", t) + testRouteNotOK2("POST", t) + testRouteNotOK2("PUT", t) + testRouteNotOK2("PATCH", t) + testRouteNotOK2("HEAD", t) + testRouteNotOK2("OPTIONS", t) + testRouteNotOK2("DELETE", t) + testRouteNotOK2("CONNECT", t) + testRouteNotOK2("TRACE", t) +} + +// TestContextParamsGet tests that a parameter can be parsed from the URL. +func TestRouteParamsByName(t *testing.T) { + name := "" + lastName := "" + wild := "" + router := New() + router.GET("/test/:name/:last_name/*wild", func(c *Context) { + name = c.Params.ByName("name") + lastName = c.Params.ByName("last_name") + var ok bool + wild, ok = c.Params.Get("wild") + + assert.True(t, ok) + assert.Equal(t, name, c.Param("name")) + assert.Equal(t, name, c.Param("name")) + assert.Equal(t, lastName, c.Param("last_name")) + + assert.Empty(t, c.Param("wtf")) + assert.Empty(t, c.Params.ByName("wtf")) + + wtf, ok := c.Params.Get("wtf") + assert.Empty(t, wtf) + assert.False(t, ok) + }) + + w := performRequest(router, "GET", "/test/john/smith/is/super/great") + + assert.Equal(t, w.Code, 200) + assert.Equal(t, name, "john") + assert.Equal(t, lastName, "smith") + assert.Equal(t, wild, "/is/super/great") +} + +// TestHandleStaticFile - ensure the static file handles properly +func TestRouteStaticFile(t *testing.T) { + // SETUP file + testRoot, _ := os.Getwd() + f, err := ioutil.TempFile(testRoot, "") + if err != nil { + t.Error(err) + } + defer os.Remove(f.Name()) + f.WriteString("Gin Web Framework") + f.Close() + + dir, filename := filepath.Split(f.Name()) + + // SETUP gin + router := New() + router.Static("/using_static", dir) + router.StaticFile("/result", f.Name()) + + w := performRequest(router, "GET", "/using_static/"+filename) + w2 := performRequest(router, "GET", "/result") + + assert.Equal(t, w, w2) + assert.Equal(t, w.Code, 200) + assert.Equal(t, w.Body.String(), "Gin Web Framework") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + + w3 := performRequest(router, "HEAD", "/using_static/"+filename) + w4 := performRequest(router, "HEAD", "/result") + + assert.Equal(t, w3, w4) + assert.Equal(t, w3.Code, 200) +} + +// TestHandleStaticDir - ensure the root/sub dir handles properly +func TestRouteStaticListingDir(t *testing.T) { + router := New() + router.StaticFS("/", Dir("./", true)) + + w := performRequest(router, "GET", "/") + + assert.Equal(t, w.Code, 200) + assert.Contains(t, w.Body.String(), "gin.go") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/html; charset=utf-8") +} + +// TestHandleHeadToDir - ensure the root/sub dir handles properly +func TestRouteStaticNoListing(t *testing.T) { + router := New() + router.Static("/", "./") + + w := performRequest(router, "GET", "/") + + assert.Equal(t, w.Code, 404) + assert.NotContains(t, w.Body.String(), "gin.go") +} + +func TestRouterMiddlewareAndStatic(t *testing.T) { + router := New() + static := router.Group("/", func(c *Context) { + c.Writer.Header().Add("Last-Modified", "Mon, 02 Jan 2006 15:04:05 MST") + c.Writer.Header().Add("Expires", "Mon, 02 Jan 2006 15:04:05 MST") + c.Writer.Header().Add("X-GIN", "Gin Framework") + }) + static.Static("/", "./") + + w := performRequest(router, "GET", "/gin.go") + + assert.Equal(t, w.Code, 200) + assert.Contains(t, w.Body.String(), "package gin") + assert.Equal(t, w.HeaderMap.Get("Content-Type"), "text/plain; charset=utf-8") + assert.NotEqual(t, w.HeaderMap.Get("Last-Modified"), "Mon, 02 Jan 2006 15:04:05 MST") + assert.Equal(t, w.HeaderMap.Get("Expires"), "Mon, 02 Jan 2006 15:04:05 MST") + assert.Equal(t, w.HeaderMap.Get("x-GIN"), "Gin Framework") +} + +func TestRouteNotAllowedEnabled(t *testing.T) { + router := New() + router.HandleMethodNotAllowed = true + router.POST("/path", func(c *Context) {}) + w := performRequest(router, "GET", "/path") + assert.Equal(t, w.Code, http.StatusMethodNotAllowed) + + router.NoMethod(func(c *Context) { + c.String(http.StatusTeapot, "responseText") + }) + w = performRequest(router, "GET", "/path") + assert.Equal(t, w.Body.String(), "responseText") + assert.Equal(t, w.Code, http.StatusTeapot) +} + +func TestRouteNotAllowedDisabled(t *testing.T) { + router := New() + router.HandleMethodNotAllowed = false + router.POST("/path", func(c *Context) {}) + w := performRequest(router, "GET", "/path") + assert.Equal(t, w.Code, 404) + + router.NoMethod(func(c *Context) { + c.String(http.StatusTeapot, "responseText") + }) + w = performRequest(router, "GET", "/path") + assert.Equal(t, w.Body.String(), "404 page not found") + assert.Equal(t, w.Code, 404) +} + +func TestRouterNotFound(t *testing.T) { + router := New() + router.RedirectFixedPath = true + router.GET("/path", func(c *Context) {}) + router.GET("/dir/", func(c *Context) {}) + router.GET("/", func(c *Context) {}) + + testRoutes := []struct { + route string + code int + header string + }{ + {"/path/", 301, "map[Location:[/path]]"}, // TSR -/ + {"/dir", 301, "map[Location:[/dir/]]"}, // TSR +/ + {"", 301, "map[Location:[/]]"}, // TSR +/ + {"/PATH", 301, "map[Location:[/path]]"}, // Fixed Case + {"/DIR/", 301, "map[Location:[/dir/]]"}, // Fixed Case + {"/PATH/", 301, "map[Location:[/path]]"}, // Fixed Case -/ + {"/DIR", 301, "map[Location:[/dir/]]"}, // Fixed Case +/ + {"/../path", 301, "map[Location:[/path]]"}, // CleanPath + {"/nope", 404, ""}, // NotFound + } + for _, tr := range testRoutes { + w := performRequest(router, "GET", tr.route) + assert.Equal(t, w.Code, tr.code) + if w.Code != 404 { + assert.Equal(t, fmt.Sprint(w.Header()), tr.header) + } + } + + // Test custom not found handler + var notFound bool + router.NoRoute(func(c *Context) { + c.AbortWithStatus(404) + notFound = true + }) + w := performRequest(router, "GET", "/nope") + assert.Equal(t, w.Code, 404) + assert.True(t, notFound) + + // Test other method than GET (want 307 instead of 301) + router.PATCH("/path", func(c *Context) {}) + w = performRequest(router, "PATCH", "/path/") + assert.Equal(t, w.Code, 307) + assert.Equal(t, fmt.Sprint(w.Header()), "map[Location:[/path]]") + + // Test special case where no node for the prefix "/" exists + router = New() + router.GET("/a", func(c *Context) {}) + w = performRequest(router, "GET", "/") + assert.Equal(t, w.Code, 404) +} diff --git a/tree.go b/tree.go new file mode 100644 index 00000000..c87e0d89 --- /dev/null +++ b/tree.go @@ -0,0 +1,596 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "strings" + "unicode" +) + +// Param is a single URL parameter, consisting of a key and a value. +type Param struct { + Key string + Value string +} + +// Params is a Param-slice, as returned by the router. +// The slice is ordered, the first URL parameter is also the first slice value. +// It is therefore safe to read values by the index. +type Params []Param + +// ByName returns the value of the first Param which key matches the given name. +// If no matching Param is found, an empty string is returned. +func (ps Params) Get(name string) (string, bool) { + for _, entry := range ps { + if entry.Key == name { + return entry.Value, true + } + } + return "", false +} + +func (ps Params) ByName(name string) (va string) { + va, _ = ps.Get(name) + return +} + +type methodTree struct { + method string + root *node +} + +type methodTrees []methodTree + +func (trees methodTrees) get(method string) *node { + for _, tree := range trees { + if tree.method == method { + return tree.root + } + } + return nil +} + +func min(a, b int) int { + if a <= b { + return a + } + return b +} + +func countParams(path string) uint8 { + var n uint + for i := 0; i < len(path); i++ { + if path[i] != ':' && path[i] != '*' { + continue + } + n++ + } + if n >= 255 { + return 255 + } + return uint8(n) +} + +type nodeType uint8 + +const ( + static nodeType = 0 + param nodeType = 1 + catchAll nodeType = 2 +) + +type node struct { + path string + wildChild bool + nType nodeType + maxParams uint8 + indices string + children []*node + handlers HandlersChain + priority uint32 +} + +// increments priority of the given child and reorders if necessary +func (n *node) incrementChildPrio(pos int) int { + n.children[pos].priority++ + prio := n.children[pos].priority + + // adjust position (move to front) + newPos := pos + for newPos > 0 && n.children[newPos-1].priority < prio { + // swap node positions + tmpN := n.children[newPos-1] + n.children[newPos-1] = n.children[newPos] + n.children[newPos] = tmpN + + newPos-- + } + + // build new index char string + if newPos != pos { + n.indices = n.indices[:newPos] + // unchanged prefix, might be empty + n.indices[pos:pos+1] + // the index char we move + n.indices[newPos:pos] + n.indices[pos+1:] // rest without char at 'pos' + } + + return newPos +} + +// addRoute adds a node with the given handle to the path. +// Not concurrency-safe! +func (n *node) addRoute(path string, handlers HandlersChain) { + fullPath := path + n.priority++ + numParams := countParams(path) + + // non-empty tree + if len(n.path) > 0 || len(n.children) > 0 { + walk: + for { + // Update maxParams of the current node + if numParams > n.maxParams { + n.maxParams = numParams + } + + // Find the longest common prefix. + // This also implies that the common prefix contains no ':' or '*' + // since the existing key can't contain those chars. + i := 0 + max := min(len(path), len(n.path)) + for i < max && path[i] == n.path[i] { + i++ + } + + // Split edge + if i < len(n.path) { + child := node{ + path: n.path[i:], + wildChild: n.wildChild, + indices: n.indices, + children: n.children, + handlers: n.handlers, + priority: n.priority - 1, + } + + // Update maxParams (max of all children) + for i := range child.children { + if child.children[i].maxParams > child.maxParams { + child.maxParams = child.children[i].maxParams + } + } + + n.children = []*node{&child} + // []byte for proper unicode char conversion, see #65 + n.indices = string([]byte{n.path[i]}) + n.path = path[:i] + n.handlers = nil + n.wildChild = false + } + + // Make new node a child of this node + if i < len(path) { + path = path[i:] + + if n.wildChild { + n = n.children[0] + n.priority++ + + // Update maxParams of the child node + if numParams > n.maxParams { + n.maxParams = numParams + } + numParams-- + + // Check if the wildcard matches + if len(path) >= len(n.path) && n.path == path[:len(n.path)] { + // check for longer wildcard, e.g. :name and :names + if len(n.path) >= len(path) || path[len(n.path)] == '/' { + continue walk + } + } + + panic("path segment '" + path + + "' conflicts with existing wildcard '" + n.path + + "' in path '" + fullPath + "'") + } + + c := path[0] + + // slash after param + if n.nType == param && c == '/' && len(n.children) == 1 { + n = n.children[0] + n.priority++ + continue walk + } + + // Check if a child with the next path byte exists + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + i = n.incrementChildPrio(i) + n = n.children[i] + continue walk + } + } + + // Otherwise insert it + if c != ':' && c != '*' { + // []byte for proper unicode char conversion, see #65 + n.indices += string([]byte{c}) + child := &node{ + maxParams: numParams, + } + n.children = append(n.children, child) + n.incrementChildPrio(len(n.indices) - 1) + n = child + } + n.insertChild(numParams, path, fullPath, handlers) + return + + } else if i == len(path) { // Make node a (in-path) leaf + if n.handlers != nil { + panic("handlers are already registered for path ''" + fullPath + "'") + } + n.handlers = handlers + } + return + } + } else { // Empty tree + n.insertChild(numParams, path, fullPath, handlers) + } +} + +func (n *node) insertChild(numParams uint8, path string, fullPath string, handlers HandlersChain) { + var offset int // already handled bytes of the path + + // find prefix until first wildcard (beginning with ':'' or '*'') + for i, max := 0, len(path); numParams > 0; i++ { + c := path[i] + if c != ':' && c != '*' { + continue + } + + // find wildcard end (either '/' or path end) + end := i + 1 + for end < max && path[end] != '/' { + switch path[end] { + // the wildcard name must not contain ':' and '*' + case ':', '*': + panic("only one wildcard per path segment is allowed, has: '" + + path[i:] + "' in path '" + fullPath + "'") + default: + end++ + } + } + + // check if this Node existing children which would be + // unreachable if we insert the wildcard here + if len(n.children) > 0 { + panic("wildcard route '" + path[i:end] + + "' conflicts with existing children in path '" + fullPath + "'") + } + + // check if the wildcard has a name + if end-i < 2 { + panic("wildcards must be named with a non-empty name in path '" + fullPath + "'") + } + + if c == ':' { // param + // split path at the beginning of the wildcard + if i > 0 { + n.path = path[offset:i] + offset = i + } + + child := &node{ + nType: param, + maxParams: numParams, + } + n.children = []*node{child} + n.wildChild = true + n = child + n.priority++ + numParams-- + + // if the path doesn't end with the wildcard, then there + // will be another non-wildcard subpath starting with '/' + if end < max { + n.path = path[offset:end] + offset = end + + child := &node{ + maxParams: numParams, + priority: 1, + } + n.children = []*node{child} + n = child + } + + } else { // catchAll + if end != max || numParams > 1 { + panic("catch-all routes are only allowed at the end of the path in path '" + fullPath + "'") + } + + if len(n.path) > 0 && n.path[len(n.path)-1] == '/' { + panic("catch-all conflicts with existing handle for the path segment root in path '" + fullPath + "'") + } + + // currently fixed width 1 for '/' + i-- + if path[i] != '/' { + panic("no / before catch-all in path '" + fullPath + "'") + } + + n.path = path[offset:i] + + // first node: catchAll node with empty path + child := &node{ + wildChild: true, + nType: catchAll, + maxParams: 1, + } + n.children = []*node{child} + n.indices = string(path[i]) + n = child + n.priority++ + + // second node: node holding the variable + child = &node{ + path: path[i:], + nType: catchAll, + maxParams: 1, + handlers: handlers, + priority: 1, + } + n.children = []*node{child} + + return + } + } + + // insert remaining path part and handle to the leaf + n.path = path[offset:] + n.handlers = handlers +} + +// Returns the handle registered with the given path (key). The values of +// wildcards are saved to a map. +// If no handle can be found, a TSR (trailing slash redirect) recommendation is +// made if a handle exists with an extra (without the) trailing slash for the +// given path. +func (n *node) getValue(path string, po Params) (handlers HandlersChain, p Params, tsr bool) { + p = po +walk: // Outer loop for walking the tree + for { + if len(path) > len(n.path) { + if path[:len(n.path)] == n.path { + path = path[len(n.path):] + // If this node does not have a wildcard (param or catchAll) + // child, we can just look up the next child node and continue + // to walk down the tree + if !n.wildChild { + c := path[0] + for i := 0; i < len(n.indices); i++ { + if c == n.indices[i] { + n = n.children[i] + continue walk + } + } + + // Nothing found. + // We can recommend to redirect to the same URL without a + // trailing slash if a leaf exists for that path. + tsr = (path == "/" && n.handlers != nil) + return + } + + // handle wildcard child + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + end := 0 + for end < len(path) && path[end] != '/' { + end++ + } + + // save param value + if cap(p) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[1:] + p[i].Value = path[:end] + + // we need to go deeper! + if end < len(path) { + if len(n.children) > 0 { + path = path[end:] + n = n.children[0] + continue walk + } + + // ... but we can't + tsr = (len(path) == end+1) + return + } + + if handlers = n.handlers; handlers != nil { + return + } else if len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists for TSR recommendation + n = n.children[0] + tsr = (n.path == "/" && n.handlers != nil) + } + + return + + case catchAll: + // save param value + if cap(p) < int(n.maxParams) { + p = make(Params, 0, n.maxParams) + } + i := len(p) + p = p[:i+1] // expand slice within preallocated capacity + p[i].Key = n.path[2:] + p[i].Value = path + + handlers = n.handlers + return + + default: + panic("invalid node type") + } + } + } else if path == n.path { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if handlers = n.handlers; handlers != nil { + return + } + + // No handle found. Check if a handle for this path + a + // trailing slash exists for trailing slash recommendation + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + tsr = (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) + return + } + } + + return + } + + // Nothing found. We can recommend to redirect to the same URL with an + // extra trailing slash if a leaf exists for that path + tsr = (path == "/") || + (len(n.path) == len(path)+1 && n.path[len(path)] == '/' && + path == n.path[:len(n.path)-1] && n.handlers != nil) + return + } +} + +// Makes a case-insensitive lookup of the given path and tries to find a handler. +// It can optionally also fix trailing slashes. +// It returns the case-corrected path and a bool indicating whether the lookup +// was successful. +func (n *node) findCaseInsensitivePath(path string, fixTrailingSlash bool) (ciPath []byte, found bool) { + ciPath = make([]byte, 0, len(path)+1) // preallocate enough memory + + // Outer loop for walking the tree + for len(path) >= len(n.path) && strings.ToLower(path[:len(n.path)]) == strings.ToLower(n.path) { + path = path[len(n.path):] + ciPath = append(ciPath, n.path...) + + if len(path) > 0 { + // If this node does not have a wildcard (param or catchAll) child, + // we can just look up the next child node and continue to walk down + // the tree + if !n.wildChild { + r := unicode.ToLower(rune(path[0])) + for i, index := range n.indices { + // must use recursive approach since both index and + // ToLower(index) could exist. We must check both. + if r == unicode.ToLower(index) { + out, found := n.children[i].findCaseInsensitivePath(path, fixTrailingSlash) + if found { + return append(ciPath, out...), true + } + } + } + + // Nothing found. We can recommend to redirect to the same URL + // without a trailing slash if a leaf exists for that path + found = (fixTrailingSlash && path == "/" && n.handlers != nil) + return + } + + n = n.children[0] + switch n.nType { + case param: + // find param end (either '/' or path end) + k := 0 + for k < len(path) && path[k] != '/' { + k++ + } + + // add param value to case insensitive path + ciPath = append(ciPath, path[:k]...) + + // we need to go deeper! + if k < len(path) { + if len(n.children) > 0 { + path = path[k:] + n = n.children[0] + continue + } + + // ... but we can't + if fixTrailingSlash && len(path) == k+1 { + return ciPath, true + } + return + } + + if n.handlers != nil { + return ciPath, true + } else if fixTrailingSlash && len(n.children) == 1 { + // No handle found. Check if a handle for this path + a + // trailing slash exists + n = n.children[0] + if n.path == "/" && n.handlers != nil { + return append(ciPath, '/'), true + } + } + return + + case catchAll: + return append(ciPath, path...), true + + default: + panic("invalid node type") + } + } else { + // We should have reached the node containing the handle. + // Check if this node has a handle registered. + if n.handlers != nil { + return ciPath, true + } + + // No handle found. + // Try to fix the path by adding a trailing slash + if fixTrailingSlash { + for i := 0; i < len(n.indices); i++ { + if n.indices[i] == '/' { + n = n.children[i] + if (len(n.path) == 1 && n.handlers != nil) || + (n.nType == catchAll && n.children[0].handlers != nil) { + return append(ciPath, '/'), true + } + return + } + } + } + return + } + } + + // Nothing found. + // Try to fix the path by adding / removing a trailing slash + if fixTrailingSlash { + if path == "/" { + return ciPath, true + } + if len(path)+1 == len(n.path) && n.path[len(path)] == '/' && + strings.ToLower(path) == strings.ToLower(n.path[:len(path)]) && + n.handlers != nil { + return append(ciPath, n.path...), true + } + } + return +} diff --git a/tree_test.go b/tree_test.go new file mode 100644 index 00000000..4e2cb7f6 --- /dev/null +++ b/tree_test.go @@ -0,0 +1,608 @@ +// Copyright 2013 Julien Schmidt. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be found +// in the LICENSE file. + +package gin + +import ( + "fmt" + "reflect" + "strings" + "testing" +) + +func printChildren(n *node, prefix string) { + fmt.Printf(" %02d:%02d %s%s[%d] %v %t %d \r\n", n.priority, n.maxParams, prefix, n.path, len(n.children), n.handlers, n.wildChild, n.nType) + for l := len(n.path); l > 0; l-- { + prefix += " " + } + for _, child := range n.children { + printChildren(child, prefix) + } +} + +// Used as a workaround since we can't compare functions or their adresses +var fakeHandlerValue string + +func fakeHandler(val string) HandlersChain { + return HandlersChain{func(c *Context) { + fakeHandlerValue = val + }} +} + +type testRequests []struct { + path string + nilHandler bool + route string + ps Params +} + +func checkRequests(t *testing.T, tree *node, requests testRequests) { + for _, request := range requests { + handler, ps, _ := tree.getValue(request.path, nil) + + if handler == nil { + if !request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected non-nil handle", request.path) + } + } else if request.nilHandler { + t.Errorf("handle mismatch for route '%s': Expected nil handle", request.path) + } else { + handler[0](nil) + if fakeHandlerValue != request.route { + t.Errorf("handle mismatch for route '%s': Wrong handle (%s != %s)", request.path, fakeHandlerValue, request.route) + } + } + + if !reflect.DeepEqual(ps, request.ps) { + t.Errorf("Params mismatch for route '%s'", request.path) + } + } +} + +func checkPriorities(t *testing.T, n *node) uint32 { + var prio uint32 + for i := range n.children { + prio += checkPriorities(t, n.children[i]) + } + + if n.handlers != nil { + prio++ + } + + if n.priority != prio { + t.Errorf( + "priority mismatch for node '%s': is %d, should be %d", + n.path, n.priority, prio, + ) + } + + return prio +} + +func checkMaxParams(t *testing.T, n *node) uint8 { + var maxParams uint8 + for i := range n.children { + params := checkMaxParams(t, n.children[i]) + if params > maxParams { + maxParams = params + } + } + if n.nType != static && !n.wildChild { + maxParams++ + } + + if n.maxParams != maxParams { + t.Errorf( + "maxParams mismatch for node '%s': is %d, should be %d", + n.path, n.maxParams, maxParams, + ) + } + + return maxParams +} + +func TestCountParams(t *testing.T) { + if countParams("/path/:param1/static/*catch-all") != 2 { + t.Fail() + } + if countParams(strings.Repeat("/:param", 256)) != 255 { + t.Fail() + } +} + +func TestTreeAddAndGet(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/contact", + "/co", + "/c", + "/a", + "/ab", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/α", + "/β", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/a", false, "/a", nil}, + {"/", true, "", nil}, + {"/hi", false, "/hi", nil}, + {"/contact", false, "/contact", nil}, + {"/co", false, "/co", nil}, + {"/con", true, "", nil}, // key mismatch + {"/cona", true, "", nil}, // key mismatch + {"/no", true, "", nil}, // no matching child + {"/ab", false, "/ab", nil}, + {"/α", false, "/α", nil}, + {"/β", false, "/β", nil}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func TestTreeWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/cmd/:tool/:sub", + "/cmd/:tool/", + "/src/*filepath", + "/search/", + "/search/:query", + "/user_:name", + "/user_:name/about", + "/files/:dir/*filepath", + "/doc/", + "/doc/go_faq.html", + "/doc/go1.html", + "/info/:user/public", + "/info/:user/project/:project", + } + for _, route := range routes { + tree.addRoute(route, fakeHandler(route)) + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/cmd/test/", false, "/cmd/:tool/", Params{Param{"tool", "test"}}}, + {"/cmd/test", true, "", Params{Param{"tool", "test"}}}, + {"/cmd/test/3", false, "/cmd/:tool/:sub", Params{Param{"tool", "test"}, Param{"sub", "3"}}}, + {"/src/", false, "/src/*filepath", Params{Param{"filepath", "/"}}}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/", false, "/search/", nil}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/search/someth!ng+in+ünìcodé/", true, "", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + {"/user_gopher/about", false, "/user_:name/about", Params{Param{"name", "gopher"}}}, + {"/files/js/inc/framework.js", false, "/files/:dir/*filepath", Params{Param{"dir", "js"}, Param{"filepath", "/inc/framework.js"}}}, + {"/info/gordon/public", false, "/info/:user/public", Params{Param{"user", "gordon"}}}, + {"/info/gordon/project/go", false, "/info/:user/project/:project", Params{Param{"user", "gordon"}, Param{"project", "go"}}}, + }) + + checkPriorities(t, tree) + checkMaxParams(t, tree) +} + +func catchPanic(testFunc func()) (recv interface{}) { + defer func() { + recv = recover() + }() + + testFunc() + return +} + +type testRoute struct { + path string + conflict bool +} + +func testRoutes(t *testing.T, routes []testRoute) { + tree := &node{} + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route.path, nil) + }) + + if route.conflict { + if recv == nil { + t.Errorf("no panic for conflicting route '%s'", route.path) + } + } else if recv != nil { + t.Errorf("unexpected panic for route '%s': %v", route.path, recv) + } + } + + //printChildren(tree, "") +} + +func TestTreeWildcardConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/:tool/:sub", false}, + {"/cmd/vet", true}, + {"/src/*filepath", false}, + {"/src/*filepathx", true}, + {"/src/", true}, + {"/src1/", false}, + {"/src1/*filepath", true}, + {"/src2*filepath", true}, + {"/search/:query", false}, + {"/search/invalid", true}, + {"/user_:name", false}, + {"/user_x", true}, + {"/user_:name", false}, + {"/id:id", false}, + {"/id/:id", true}, + } + testRoutes(t, routes) +} + +func TestTreeChildConflict(t *testing.T) { + routes := []testRoute{ + {"/cmd/vet", false}, + {"/cmd/:tool/:sub", true}, + {"/src/AUTHORS", false}, + {"/src/*filepath", true}, + {"/user_x", false}, + {"/user_:name", true}, + {"/id/:id", false}, + {"/id:id", true}, + {"/:id", true}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDupliatePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/", + "/doc/", + "/src/*filepath", + "/search/:query", + "/user_:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + + // Add again + recv = catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting duplicate route '%s", route) + } + } + + //printChildren(tree, "") + + checkRequests(t, tree, testRequests{ + {"/", false, "/", nil}, + {"/doc/", false, "/doc/", nil}, + {"/src/some/file.png", false, "/src/*filepath", Params{Param{"filepath", "/some/file.png"}}}, + {"/search/someth!ng+in+ünìcodé", false, "/search/:query", Params{Param{"query", "someth!ng+in+ünìcodé"}}}, + {"/user_gopher", false, "/user_:name", Params{Param{"name", "gopher"}}}, + }) +} + +func TestEmptyWildcardName(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/user:", + "/user:/", + "/cmd/:/", + "/src/*", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + if recv == nil { + t.Fatalf("no panic while inserting route with empty wildcard name '%s", route) + } + } +} + +func TestTreeCatchAllConflict(t *testing.T) { + routes := []testRoute{ + {"/src/*filepath/x", true}, + {"/src2/", false}, + {"/src2/*filepath/x", true}, + } + testRoutes(t, routes) +} + +func TestTreeCatchAllConflictRoot(t *testing.T) { + routes := []testRoute{ + {"/", false}, + {"/*filepath", true}, + } + testRoutes(t, routes) +} + +func TestTreeDoubleWildcard(t *testing.T) { + const panicMsg = "only one wildcard per path segment is allowed" + + routes := [...]string{ + "/:foo:bar", + "/:foo:bar/", + "/:foo*bar", + } + + for _, route := range routes { + tree := &node{} + recv := catchPanic(func() { + tree.addRoute(route, nil) + }) + + if rs, ok := recv.(string); !ok || !strings.HasPrefix(rs, panicMsg) { + t.Fatalf(`"Expected panic "%s" for route '%s', got "%v"`, panicMsg, route, recv) + } + } +} + +/*func TestTreeDuplicateWildcard(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/:id/:name/:id", + } + for _, route := range routes { + ... + } +}*/ + +func TestTreeTrailingSlashRedirect(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/no/a", + "/no/b", + "/api/hello/:name", + } + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + //printChildren(tree, "") + + tsrRoutes := [...]string{ + "/hi/", + "/b", + "/search/gopher/", + "/cmd/vet", + "/src", + "/x/", + "/y", + "/0/go/", + "/1/go", + "/a", + "/doc/", + } + for _, route := range tsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for TSR route '%s", route) + } else if !tsr { + t.Errorf("expected TSR recommendation for route '%s'", route) + } + } + + noTsrRoutes := [...]string{ + "/", + "/no", + "/no/", + "/_", + "/_/", + "/api/world/abc", + } + for _, route := range noTsrRoutes { + handler, _, tsr := tree.getValue(route, nil) + if handler != nil { + t.Fatalf("non-nil handler for No-TSR route '%s", route) + } else if tsr { + t.Errorf("expected no TSR recommendation for route '%s'", route) + } + } +} + +func TestTreeFindCaseInsensitivePath(t *testing.T) { + tree := &node{} + + routes := [...]string{ + "/hi", + "/b/", + "/ABC/", + "/search/:query", + "/cmd/:tool/", + "/src/*filepath", + "/x", + "/x/y", + "/y/", + "/y/z", + "/0/:id", + "/0/:id/1", + "/1/:id/", + "/1/:id/2", + "/aa", + "/a/", + "/doc", + "/doc/go_faq.html", + "/doc/go1.html", + "/doc/go/away", + "/no/a", + "/no/b", + } + + for _, route := range routes { + recv := catchPanic(func() { + tree.addRoute(route, fakeHandler(route)) + }) + if recv != nil { + t.Fatalf("panic inserting route '%s': %v", route, recv) + } + } + + // Check out == in for all registered routes + // With fixTrailingSlash = true + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, true) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + // With fixTrailingSlash = false + for _, route := range routes { + out, found := tree.findCaseInsensitivePath(route, false) + if !found { + t.Errorf("Route '%s' not found!", route) + } else if string(out) != route { + t.Errorf("Wrong result for route '%s': %s", route, string(out)) + } + } + + tests := []struct { + in string + out string + found bool + slash bool + }{ + {"/HI", "/hi", true, false}, + {"/HI/", "/hi", true, true}, + {"/B", "/b/", true, true}, + {"/B/", "/b/", true, false}, + {"/abc", "/ABC/", true, true}, + {"/abc/", "/ABC/", true, false}, + {"/aBc", "/ABC/", true, true}, + {"/aBc/", "/ABC/", true, false}, + {"/abC", "/ABC/", true, true}, + {"/abC/", "/ABC/", true, false}, + {"/SEARCH/QUERY", "/search/QUERY", true, false}, + {"/SEARCH/QUERY/", "/search/QUERY", true, true}, + {"/CMD/TOOL/", "/cmd/TOOL/", true, false}, + {"/CMD/TOOL", "/cmd/TOOL/", true, true}, + {"/SRC/FILE/PATH", "/src/FILE/PATH", true, false}, + {"/x/Y", "/x/y", true, false}, + {"/x/Y/", "/x/y", true, true}, + {"/X/y", "/x/y", true, false}, + {"/X/y/", "/x/y", true, true}, + {"/X/Y", "/x/y", true, false}, + {"/X/Y/", "/x/y", true, true}, + {"/Y/", "/y/", true, false}, + {"/Y", "/y/", true, true}, + {"/Y/z", "/y/z", true, false}, + {"/Y/z/", "/y/z", true, true}, + {"/Y/Z", "/y/z", true, false}, + {"/Y/Z/", "/y/z", true, true}, + {"/y/Z", "/y/z", true, false}, + {"/y/Z/", "/y/z", true, true}, + {"/Aa", "/aa", true, false}, + {"/Aa/", "/aa", true, true}, + {"/AA", "/aa", true, false}, + {"/AA/", "/aa", true, true}, + {"/aA", "/aa", true, false}, + {"/aA/", "/aa", true, true}, + {"/A/", "/a/", true, false}, + {"/A", "/a/", true, true}, + {"/DOC", "/doc", true, false}, + {"/DOC/", "/doc", true, true}, + {"/NO", "", false, true}, + {"/DOC/GO", "", false, true}, + } + // With fixTrailingSlash = true + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, true) + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + // With fixTrailingSlash = false + for _, test := range tests { + out, found := tree.findCaseInsensitivePath(test.in, false) + if test.slash { + if found { // test needs a trailingSlash fix. It must not be found! + t.Errorf("Found without fixTrailingSlash: %s; got %s", test.in, string(out)) + } + } else { + if found != test.found || (found && (string(out) != test.out)) { + t.Errorf("Wrong result for '%s': got %s, %t; want %s, %t", + test.in, string(out), found, test.out, test.found) + return + } + } + } +} + +func TestTreeInvalidNodeType(t *testing.T) { + tree := &node{} + tree.addRoute("/", fakeHandler("/")) + tree.addRoute("/:page", fakeHandler("/:page")) + + // set invalid node type + tree.children[0].nType = 42 + + // normal lookup + recv := catchPanic(func() { + tree.getValue("/test", nil) + }) + if rs, ok := recv.(string); !ok || rs != "invalid node type" { + t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv) + } + + // case-insensitive lookup + recv = catchPanic(func() { + tree.findCaseInsensitivePath("/test", true) + }) + if rs, ok := recv.(string); !ok || rs != "invalid node type" { + t.Fatalf(`Expected panic "invalid node type", got "%v"`, recv) + } +} diff --git a/utils.go b/utils.go index 43ddaecd..7e646876 100644 --- a/utils.go +++ b/utils.go @@ -6,11 +6,44 @@ package gin import ( "encoding/xml" + "net/http" + "path" "reflect" "runtime" "strings" ) +const BindKey = "_gin-gonic/gin/bindkey" + +func Bind(val interface{}) HandlerFunc { + value := reflect.ValueOf(val) + if value.Kind() == reflect.Ptr { + panic(`Bind struct can not be a pointer. Example: + Use: gin.Bind(Struct{}) instead of gin.Bind(&Struct{}) +`) + } + typ := value.Type() + + return func(c *Context) { + obj := reflect.New(typ).Interface() + if c.Bind(obj) == nil { + c.Set(BindKey, obj) + } + } +} + +func WrapF(f http.HandlerFunc) HandlerFunc { + return func(c *Context) { + f(c.Writer, c.Request) + } +} + +func WrapH(h http.Handler) HandlerFunc { + return func(c *Context) { + h.ServeHTTP(c.Writer, c.Request) + } +} + type H map[string]interface{} // Allows type H to be used with xml.Marshal @@ -56,17 +89,20 @@ func chooseData(custom, wildcard interface{}) interface{} { return custom } -func parseAccept(accept string) []string { - parts := strings.Split(accept, ",") - for i, part := range parts { +func parseAccept(acceptHeader string) []string { + parts := strings.Split(acceptHeader, ",") + out := make([]string, 0, len(parts)) + for _, part := range parts { index := strings.IndexByte(part, ';') if index >= 0 { part = part[0:index] } part = strings.TrimSpace(part) - parts[i] = part + if len(part) > 0 { + out = append(out, part) + } } - return parts + return out } func lastChar(str string) uint8 { @@ -80,3 +116,16 @@ func lastChar(str string) uint8 { func nameOfFunction(f interface{}) string { return runtime.FuncForPC(reflect.ValueOf(f).Pointer()).Name() } + +func joinPaths(absolutePath, relativePath string) string { + if len(relativePath) == 0 { + return absolutePath + } + + finalPath := path.Join(absolutePath, relativePath) + appendSlash := lastChar(relativePath) == '/' && lastChar(finalPath) != '/' + if appendSlash { + return finalPath + "/" + } + return finalPath +} diff --git a/utils_test.go b/utils_test.go new file mode 100644 index 00000000..ba0cc20d --- /dev/null +++ b/utils_test.go @@ -0,0 +1,99 @@ +// Copyright 2014 Manu Martinez-Almeida. All rights reserved. +// Use of this source code is governed by a MIT style +// license that can be found in the LICENSE file. + +package gin + +import ( + "fmt" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" +) + +func init() { + SetMode(TestMode) +} + +type testStruct struct { + T *testing.T +} + +func (t *testStruct) ServeHTTP(w http.ResponseWriter, req *http.Request) { + assert.Equal(t.T, req.Method, "POST") + assert.Equal(t.T, req.URL.Path, "/path") + w.WriteHeader(500) + fmt.Fprint(w, "hello") +} + +func TestWrap(t *testing.T) { + router := New() + router.POST("/path", WrapH(&testStruct{t})) + router.GET("/path2", WrapF(func(w http.ResponseWriter, req *http.Request) { + assert.Equal(t, req.Method, "GET") + assert.Equal(t, req.URL.Path, "/path2") + w.WriteHeader(400) + fmt.Fprint(w, "hola!") + })) + + w := performRequest(router, "POST", "/path") + assert.Equal(t, w.Code, 500) + assert.Equal(t, w.Body.String(), "hello") + + w = performRequest(router, "GET", "/path2") + assert.Equal(t, w.Code, 400) + assert.Equal(t, w.Body.String(), "hola!") +} + +func TestLastChar(t *testing.T) { + assert.Equal(t, lastChar("hola"), uint8('a')) + assert.Equal(t, lastChar("adios"), uint8('s')) + assert.Panics(t, func() { lastChar("") }) +} + +func TestParseAccept(t *testing.T) { + parts := parseAccept("text/html , application/xhtml+xml,application/xml;q=0.9, */* ;q=0.8") + assert.Len(t, parts, 4) + assert.Equal(t, parts[0], "text/html") + assert.Equal(t, parts[1], "application/xhtml+xml") + assert.Equal(t, parts[2], "application/xml") + assert.Equal(t, parts[3], "*/*") +} + +func TestChooseData(t *testing.T) { + A := "a" + B := "b" + assert.Equal(t, chooseData(A, B), A) + assert.Equal(t, chooseData(nil, B), B) + assert.Panics(t, func() { chooseData(nil, nil) }) +} + +func TestFilterFlags(t *testing.T) { + result := filterFlags("text/html ") + assert.Equal(t, result, "text/html") + + result = filterFlags("text/html;") + assert.Equal(t, result, "text/html") +} + +func TestFunctionName(t *testing.T) { + assert.Equal(t, nameOfFunction(somefunction), "github.com/gin-gonic/gin.somefunction") +} + +func somefunction() { + // this empty function is used by TestFunctionName() +} + +func TestJoinPaths(t *testing.T) { + assert.Equal(t, joinPaths("", ""), "") + assert.Equal(t, joinPaths("", "/"), "/") + assert.Equal(t, joinPaths("/a", ""), "/a") + assert.Equal(t, joinPaths("/a/", ""), "/a/") + assert.Equal(t, joinPaths("/a/", "/"), "/a/") + assert.Equal(t, joinPaths("/a", "/"), "/a/") + assert.Equal(t, joinPaths("/a", "/hola"), "/a/hola") + assert.Equal(t, joinPaths("/a/", "/hola"), "/a/hola") + assert.Equal(t, joinPaths("/a/", "/hola/"), "/a/hola/") + assert.Equal(t, joinPaths("/a/", "/hola//"), "/a/hola/") +} diff --git a/wercker.yml b/wercker.yml new file mode 100644 index 00000000..3ab8084c --- /dev/null +++ b/wercker.yml @@ -0,0 +1 @@ +box: wercker/default \ No newline at end of file