From 0c1f3c4e81f6e969a3e465d933faefb5578c54d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Wed, 20 Mar 2019 12:07:34 +0800 Subject: [PATCH 01/10] chore: fix invalid link (#1820) --- README.md | 4 +++- doc.go | 2 +- testdata/assets/console.png | Bin 59545 -> 0 bytes 3 files changed, 4 insertions(+), 2 deletions(-) delete mode 100644 testdata/assets/console.png diff --git a/README.md b/README.md index b46c5637..d3433ed2 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ For more feature details, please see the [Gin website introduction](https://gin- ### Getting Gin -The first need [Go](https://golang.org/) installed (version 1.6+ is required), then you can use the below Go command to install Gin. +The first need [Go](https://golang.org/) installed (**version 1.6+ is required**), then you can use the below Go command to install Gin. ```sh $ go get -u github.com/gin-gonic/gin @@ -111,6 +111,8 @@ You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin ## Documentation +See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package. + All documentation is available on the Gin website. - [English](https://gin-gonic.com/docs/) diff --git a/doc.go b/doc.go index 01ac4a90..1bd03864 100644 --- a/doc.go +++ b/doc.go @@ -1,6 +1,6 @@ /* Package gin implements a HTTP web framework called gin. -See https://gin-gonic.github.io/gin/ for more information about gin. +See https://gin-gonic.com/ for more information about gin. */ package gin // import "github.com/gin-gonic/gin" diff --git a/testdata/assets/console.png b/testdata/assets/console.png deleted file mode 100644 index 7a695718fa31f9b1b42cfa547c16da394378aeae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 59545 zcmagFRahKdur*8q!6is=hu{+22Z!K;Ly!sXF2M(P4Q_+GyF+jbOmK(b?hb+ZdCxiD z_1_nL(a+Oed)Ho7d+k-#5kO^G3{+xNI5;>Axvx@caB%N=;Naf*AtSy$@iIOxhl4x# zE+-|f;jw(OG^N}ph9|8W!1N9H4_0%FLT|N`vXVY+ydbAVnIt=Lg3fOK|9_<{+yC=) zdO?Sq-nusZCtPnQ6Zru5Jxc0N;zSGzoFa`!%7(qn^D#4ArMf`P9;Nd<1*doK`(KFt z7VM5%wV&1+NfFPZdhg*Ve+tQt?Ykhv{nE7?iN>XbvmF3P{$(NLLK{#Z5P|piy41Zs zSj6UIMozb_49A6otj#`P^T*6Hng6_;R=PojQz%D=R7>w5XL=g%eJsaH;e*p@db$vh z8kiU{k4$$O;5Wab!Yz$RdQAmOz+D|dlYLHh;OQy&I>K|? zu({IX$luHC9EJeeKNGLPCXFcTv67eQ2^sk>=O;?Ir=9cKC~ z6LOkjrM`?0yx8>TpAk8fu20m+S*EMAp)t9SM&}B-6)g2`hkk4K)wlCrEzbbi+2%nR zyn%Pv*tSS`ra!-+a26n%LM)OzktKt_n7xq@b$eZxpHwL*FG)1%lmiOP3Ijg zdhL1#cYEI63xaM?mD|xA0Ah$emW^-4QH{|;^|EGDkNPdFmydIXH^M5j_1`q^(bmSx zML(A4zl-gTJi9F#N#DL1-qFjD)qiOb6DqD3Ysg9~3yR&iM9IHOccgPl|84L(#!>h^ zY=*tv5PdiDpQwH73vkFbP?Yr+)J`dRVPDS|8gu>}fK8gb`XFIT2um8+yTc}a`C;Kh%hmJ!AZNgX{)^zw z&O0*Uga3PT9$t$KnwX5pVY!#GjY(k^2zuGov@R{pY&}wu?RO zZ`b1AL_hnc&gaNc48O*{KqgUQG25nunyf_YrZzEQtHClkzrP5Z%A8wU{d>5)@4lVa zyFchuDYz$}=)!qrru*NI+4K4E$4vS5$oSTTzK}nQzA!Vf9aHfN?(F3xFFyF4x;~@C z0Me&^Vz-D>(Ejt<9*TjUtam+ska2W$gr2Un^t9S6RO^Wyb`d7s{~VaF((^uFZEN5Q zyZPsAllu?we=B!+F9yx2g(mpP9>*2N8H0-gUx=SuUY_rJe!UI-&FobWs=zco-v(~o zV_9SeWOh|fqTnZyd7H^lGT>~;anptnwTz%*HMoQjZ#Vz5w*`egvd|1*xim!vhoVp#Tr?Ta>>~>DD`@nnUxJiWW zv_-T8{Q9`h+UWFP4W7JTdtE=>YJri}_NbkQHHwD$JwKnq`0PO4KDtND+f4R`J$__L zN=ig0SflLq;3SZqr$F13K+|QwVq@y;zf$Cw^~30Y!m%5q`!|D81oxC`*PnHcV$@)C^bJ~W z%wXa<5wu5^4|D>xRFLg?Suq0_MVFVHAA>+Gd*(ea=bbOUM0yk_u%Ie%bMpSc(D-3o z2PQ6M^S$;7*3y5I>Q>vI-5Zv&y^)L2_$INxJSrah&6Nxn_kI zA209v-%+uh;t)8D#Xsogb(X~_zlM6gUba7Z1r!g-K=`xh#~QUj02d;nD|64?1}MY2 z|5GSpitMG#6yh<>xV!*oQv4(5q+426U9{KFIpBuBOr*kac|t)bXY~Ta$KAn0Us*b_ zIQEc;n+(OlG|^;2UjY_WRvHVG17cn>wWsvi2!<{#>Ll`zU66 zQ=O+_@AxBuctgd?Z?bhQSmrYqf_ZM7>-qYNc2C zE?ML4wAjUqGk83TfZzkiR;Fxy8D?1wqRIx3Jvm)*H_TzF@6-8H9&TP1op-7PiiS6A zMbi1xn!Q(rm}+dIHoN6cc6yu!6Y9GofO0#_&8eflpMiK{YN)yZ!eXeC|ITlsP8ZuWRiWF5@g`9})}qXn%@ zvSY0@cd1Xab8~B$&kb33OFft8P2fbwk1-gxrDbKC6qFKBnaLlx#FGWS2yUItgIGUh zn(RWUwaK6|&N8V89Nn&U5l`ARM>J#^Vfx(Oq*)9L%`Y#<8y3&wt~v8DOzKTN&|?tB z?LtqqN8?PBe_abJw-zhXhUZb?pkU=V-ayV7sQBg$s@S?t6NS+cUMrdN}9TvEWUvq0L~-U5QuW6R}{taT8@r$9p}5Lf6!WUIeB z%W<+3aA>KZfpea-r@j5#d?NJB=^1dMvb{`6N^r<}#ME41aqcGK3(INrdgU4Os-N75 zd(jQ$zpRv&|3{_h=|H%CB8psdOXA<<#8_%aiKMR=GcfYKlgsiW;*B@_3sQ{CS1Fd0 z_&T{=HS7+;?4YcnkNH;fRVqHKK1ToymqS$S$Mu< zQ8D5|30AeaWp(00eoxoB;1_jI*pq|FOU3b|-iGV*nePj;9fOlJWLEu0Ryu8Jyn~?E z4Fe7i&PNIg_P&Dy%lPwcXI3eH53;rI|GWO5>pat5O8AWXEr&IepbJ2zmR7VH$jwH< z)U&N#+@FzSx?9)KP2Sm>9G|OEW!p;6>%Fcc=QjKJ%cW9Q1k2vch<@wd^4_3?`YAVc~1UGtE^$O(#m9d6cv0%NfLs@d7~EN{;;6TWN;<(qj5eQF2l zZNOQizst)*0@~JW&0q*nNUg+dH8v(hWHfSxRhIlQIAVX&-Vk8 zBX067tZ<|%x9j~%zwzkgQn(OKsPB}Vu1exBe(1k*EcA^o-9gVC>!UR84B*!;B{q5 zyp~{Q?Eu=kt>coBk*@6SM?lI+gP7|I@Ele|E? zZZ~SI_CL+8f#nOXm+pFwv5DVMbiuI6AyG1!Ed&dlv6#rXZJ#;p6vy?CS(fm}w>?So zef}s`eFO%^8+-bfe;TgX7#o;4xDTZyFIX}Gs`Ltj76eyjK-2JhdtDh zU392ZY+((}J#-!7qNe@_yCi}UvsM-r!IY9@#vEzGfz1~WPfsM$FV#}c{q|Y*$l_`* zn4#hH3+H`)W5)_+eORaag5`eY)%6&Um3ubVv(C;_POF)dSv6xCIZbYjdCEjJPoAdQ zo~N0D)$M|1rIghq#06O5a;erIEr5H~7o-Rq3^B917T%>Au3Nm5?wz{DDuH-enn5j$ff?PmxeRi3)P zTLckVq=O>3RC7@2eGW9}BBLTpt5C(;t@U>$hPN;2RV^Dw1elOhlAjW?lET$TrSPK0 zE$!#>`T2+fg@dIEYFi1-D}q*R*IwBuPF1YtxX(VN2zQRs*FEDp-i~i**c9K>Ay5_wj{cOuo06wZo{avZ$_R$5T?OggiM#!q%|o zovySNwV7r-uN|Fni^c0lpW4swHQizg6+V8nR>Yc}d3%Kfa&;to!0SO#~W@8v;Yp!$q?)18ZjBQD*)(4Ysl@ zU~?7<+hw^9|Bji5cP#HlPcKf{2eQtX>(7BglgY_UGkB{*(H%Uinu_$|^6nHs%@)zqYBK{u+?Wa| z)xPs$%2n?|80Gk#zu;V=T=HWRMarapTBzpTH10wh&hK+#peB`wH5X=PvXW%tGugc0 z`(plNix7(@c7J9dKJ!Vu3cO?Su-#GaT0 z++vyy8Rbd)f#pYB$Bo3UFQu^Y2dn!Zj=UY=q(4N^4@lJbV&@)BNy8--<1_4CoP;7*pX;`S4*8fy53 zS@D*R4>hX%*k5CugJ!ar#1*18U|M?cx@nlmuv&U$gtNIMT_~&^9-xDdZd3Gdi(XQM z97T60zA$wSG_C#^EOnra&`{luvtoh%3n{Z-Raa?X>~Ko`Xso_LIW+1q!SF;((g0)s zaMIfjES-)@48JeB^;FY2a}1hDY;T#DT_Qm7?dq(d{|qXt5ld-VvR!Pn!p+amM<_!< zLz6T%&Zp$w+}xZdX#5zlCCIl(Q4ex`9vz@6MLTSFz2aO zXOf!*edUz=j=0>WUZSkxyc|u$wO+q+%D0MJ5)xCncEMuKb=R|mRZRdHTnO6ebE9TQ zA)^q#>{$=`3GPbW!rFei=xqfwit3Bx?AGVUruo>@|N5@`&z3XO(b7+J_QVSX=H!e} zos%QRryS6#%8$#3&LuEuMO~9IMryc7VIHMB_C36Hc1F=o$$-Fr*J9XM_7MGq11j5U zf`TciP5JYqIVkYsjE^n*Bl5V~kfUyOzq3bQ1xcqC$ZnCz@}%>q*uIa6f8(syrL@>4 zuemobsZ6j~%dG4LIPNlHXSrLkJ15Kr)!jy_TU#cUNn)SZrO%{G9q-8?SlsSoTjVQd9PW%dI_LxM@^zoo3LfmY-FD=MNP7u)-#`c;IYbFbA*(1f1XPx3L( zv~!2)Aa|s4>vBgrb5Y3g6AmDYgA9J-B+jqqUS|00<)bfCP<8+zL1xiM@ndb1r|0d# z2R1Ch@S{+=qaGcTz%PPIZG>w@@WpIcp;J2HpK(D$QMjqOzjf(H($Xvtg!0P!0>~N= zH~U$o{3^;E+&kv+XSJ!t_TERIuf5ATiIL?QExAlKsj8Mkaq~I*_tXOGuLf%tFN2%T ztwFO+4!)#W5HD+RBd3{nJ-aZUr@hNa2`3d5)nbDM*7VHGAlks2ptD$K61F=04f(q$ zAtB)d{(pSqncmr6O6|oJsO`cGkY~Q)N#r|y*^#<$Rq>pk$4aXX^<<3Nf*UJ&J(*n z(=^o<=mLlQo?C>xm6<{N>x4lp1qS-s(kE$Qpf3FIdgse(O!>*re-FbTVmF8Hx#K7E zGDlUxcnB5Q$nWf|Ka`kEb^M^9-N$BRZ%H()@0tDKf_-Ru_a|QvFzn?FZe^aAWl$P znq{Q{CpPTPhw`M_x^VCmSIX${iR`NqRJ_d*Pnkbr7UAY3;=*JfsIJFqH<4Ex)o(p? z8RT7SG=sm~+0tOB2EyLxHYhYA30Z0RV6;|DS!n*LSNF_cb8#T@s2nvb92Ngm1w@YE zBF$P(DpLso5N1Ci<>y8(*STbV{who!wYX?mnITG6)5%M!N)L@lB(MSzVAF?jAh`^8 z#gycn1xBDP5~M2_Q!mUkOXo-3&y99`vKrEB2}+pY#vz0MkGko^e?XTB*A6~8o{b}TsHXkb=+F!OtBKlum2R7j@B15i{05h)mkVy$X z7MgD%%%UHJIlM2~szKY|TFy$Z?fjE?L0vtO9pXU1{)=x$R0@Nvj3VO;xtR=&ii|2~ zj5A%46g3d1D`96!^A80^K&zF3zk?0$RuB6b{zJ1njr}tI-?)$&pn_Sfeb!e_DFZ}Y z9L7}29pl4ED;=;OC#GO3O}9DObmo2TgLR|WQ;|q`h2gO0Tezg0ASYe5R?n)8CB;M4 z959Ib!^}*oLS?kbd!nxKt5ibgw!25;t}+{Bysz(Oolg zT7;#>;so*rF9|ymi+I^6T{W6oEIFkb7lN*t`{Foit=X=vFs!fQ!Q;i81tRjH20g?7 z#@IQ37hkpI{KD)@Mpd8gUQVodi)_axLjo~iR6wlId?cDOjRkYjN|HrUAXfYNPTW|F zaqLuB#`(8Z$8G5m>FI#s9)9m$IqY^%1t0LV$W3)B|$|?NTh3+0LeS{ zp5Q+PjEz=k&gVob>!lI1SrJ0fe`+14In<8E9FG~E#QDP|lX}>>aR{6hzPEUuY|&09 zd$FK5+WpmNMoCsiVye*R2Md$-c+?`rsH2S8X#qrVO4PTG6H&S5q2z(p{d@VrLvu`~ z>0jyVDX{x*uZJW2SXK$&yLf)yyWkmn+RC)u;WW`j3m{8=@TQRo zjF`rCKb1JE=yLLX#PMlxBI}Mp?guG<-eMo6zdX%D_GvJqsjm4Jt1nn4fL%|5522(D z;`@eD@Vq2}b|f{+wsQ`~Tu^dxEU^Cc`m|)^^9-9t-e>UpT4=xr74Hq$Z)0)DC{#fy zBxS5xDh z2oh!!wpSfcbY+H`px+E;ITcq6ZPV#@$Gw&t`)O1rh7J1?O8*I6RkUj|z|a z^X6a6*_QoAxHE(tH3boj+7<_LRCxRKv%&(iuZ`9^@C1;4S{(jfKt+&FO9C@{$X=5>n1ZZiQqA+kv1O|fL%>r(UdoO0oLYSB*{WBmocoG@nn zX8Uvs5ojh^GbE2;DZogU3a4&{_A~brxEP3MT>Uh;Fm54m+|ghwXSPLN!I;-A9WNqUczJ2M$C0+b|zNF3WoO|o8ray~pEJZ~`k?zil`S2Z_ir|hG7ACDpaK^Fi1wn^K?Fa0GU1j z36-5sc^IPJDOC+|cM}{IfU|MQefqKcW z*C}p&thG2%`s@Lab3O9X!}Xzht1|5{zy>jYBWKL44RgWF)431rJ@t-S=b!j6lU`h4 z0w)5v927(XqWfC|OJ;Z6jzW;vL$f&gIDi-@EAGcOYE1N5jZSi)2=bQh%|^I=yw2cv zc+X@{wRKycyfeinw9`kdR>M?g)`uh40`hGcM+>}g)bPj6M4Aq(O5_+0m-)wM$L5Q# z(?*2T*{KL}eY9>h+&-Ggi=0;6Hno?&Oz#n>mNtnFYw9k$B2pY`X)QXnwP7p8G_^|X z^l2o8t*)V|wmUFRM=*RTYm+MBAz&=zJULi(7B*4W(5GIX(8ar;Hh()S;3Q)3s-sA> zOb#hX66Ds-qE}1=&Zv4{szC!rIE3PHy0GFz7C+?;|4hgrjnlkAWaOP#y$P2i6XOIVk8QzU{`3t+vU(>ZBnMrW zjq>C8qD!0R1Q!^EhezUq_y@yIe?bMi{1elQYKa=YTouuOad=@f-P_3nPNK!Vlo{k< zp(Dae&S~zA0t+|lUn6FheuwA?m{G1nI^p)~F&IXa`?K!-qEMn*@@9|3QRd}N3OT+MsZyonU0WCIS*_hc(~;L(F~s?rb5;sbk#-P zb-rjn{rLa{JJo@GTPm+Udfl)F7oyvN8aWF|PaB%qpddex`9`5B${@1%l5Fo~{QLm&BUI*zoiRo(we+5dS7UMcS8By#_K(=XSsAf0c`otEnT*IW z?R5lrc$R#g>Fn2yht>jB4S(yeblnII$#Mh%(q|va$L8*I#}YX;mris{^_dlv5)UgI zGT2XR&Koh}Z<({)saY{BZA%At$5Vk?X;E3(E=H>jgooHGstdqH@ z%%OFyaK`{IEM`EARKgT`LmhIMd)2^V+&opy*MH@GON)-t-s~RZquTOBN% zV?>j}s3a`8G6g*GhJ0#C5tpS3B(yfy4v*tO`b=5Ilj%CqA0HWw>=Vc)L?E(yOdz;$3{i`6B3%}DsF z{D6s`cfTg1lo2~{?aAF@g-P7G0B)lnuZxp?v! zf#|X5W9(3TR9s1%B+Q70!*5jwo!a+3LFM41zO&THNI0Px?eO=Z&g2ocI@J2K)Kq%q z0C|@PN?&dQIf5%`U6Y7+2|3k$fBmP@9-WoO!9uys8u5+R&dbD6g;o zr-SCe{_R6WZvIHJ1OKJhnGDB{jE=U+4ees&hrl5we!y&KXh=|@f~j#&OO4P8f^CtjL#t&Y&j>^3_I?o2F+-=98S(a4cg@}{wucK zW>lbc{RI#5e#6Q6;43{@SiWz^r}3 zb81U`9O?Pp)FeA*(=+mRj?;_FD^}O*Zi5z#_a5xV63n*R-QMD1o7Vy3N*lPa!U&R^ zc&o=evLh)NFQ>db4h!Gk?j1}O$ku{PVIzKORJ`l!b(ek^MSd8&&sG!J^gO2zM~BVX z(+uZR!c)X~R5u5Ub=y%l|42Dz5^g+K-h>sB?hwAZ-@g?vxK!}{_B`;5E)k8lpm~p6 z{9%&6j2a>u^$p=PQpO!Uv4ZteX!`Vj07S3N4b7=Nw{U+sBMQ;2!la-jA3N81uW+2F(Rn=38 zZPF?Z{%s<#+-+l5a8!9jH#6Hx%xS7uwFF9LUfENjeH>3M#C3W4w&I3#T~mWJHQ*G&2Y zO!SWhIkBVW=3mfa&OV@p@e~@5`AtZ+DU;(qV!k{{A#X(T-M(+!9l@@%bGm=sb)GAe z>?5(ahq6$bn3t(d{AV+-Ocm1dyA+fCiPtOLrewiaPL;l%SBH(K1tO%i7m!k_70wv{ z;WP&%FM-Vuf8$vm~o3G3WulCb{CSP#gX_G z*M*6sb~H;jn~B$dm1FxBDV|#cC~tMezaILen}o3xak4(fII*8;so6FWw&Eycef~KO zsFJ4&_&zn-!sffp*Ewa#4_0#A{>*Q;Hj8pr$BtKB`RP2{>=!br%qbmx-i`!NPtZSG z`0+iv=&?vv1T{ymU=kV5gHzgHyH++#m^T)BeC`UZRuVkbEMQMPO2?o9Ir7mvH&`mv z+B0Q1uli-~rm&FK4;*@rk8(-+R_ugH2hPr|yh1IF$d<5T2|0|ppQZ2OPUvsLW4thS zPaB6;6#BdWtz~p%&;C>F3#*`q5~z%g_X_?Gp%$*9;)Zr$%pC_Hk$a?1d%tysMGO2m zTLrBqR))~ii(IR6hS^$Tryk-t6Au${s-z^Mi4~7pgjUiMq_Xr6$i%pK>T1o;9p&0> z&)>mBy+QBkv^A?%H1~l9bJsc|P<2ghH#ZECABZ=G@W>fxSjdT-5!%w1fnFOOPmg#v z5i08))LyDP)ap1P`ge(Tb}RQ%Y_F);->hna>Y7^Y&ngq3;3OWM z>um^h-7nc-9cOjDezQ64qTqThafcHvHQKxw0iGfo2-dcAYDMRC7BaKzG1)s)p!k@qwQoTu491&ABYDN-UPcc%b{G^Z9 z+tP_H@~;$cotq2yIX__>)UdEP%*Vv2-z>2a9-})wrqOGwhb1|F_(#&Sai#PazK8q% zF|yG$36-HZfuU{5K;|FfWm2+5MRd9giLG0szvXEVk#=t^zPUZkEm0U%AFzr-C51xU>7KLW0215AI3<$pvcGUAi99+nZ^L{zbKXxUaWc0Q;!!pGvg3$K z?s^?!Img-qcWSX2Bjj?V;CU=hgA$0>k-+!%%x$F~uHf^o`z;ZiA`{}ZYgi`Er5Fxu zI5nmQ?eMKPH18Q6vQ%$|24gAS7D=Xh^-wXBGSrBz_IVqh_P(^$1LD*Ape+y(4Pnx3dS}g z-)Me_%cZ!3({g3byl})Z-Vu+U^-k*$Ps#TWPhI%SXwCt``s+}yIz-5!o~aumeO}5SB6|AC~2UazW0`PBXx*{KkbK5fWH@pI2Ecv)O}>W zJ{&YOAYW`bfmWegTA^XnH&QJrAFqf(T9#Ehpsy4QgRBkzw$N!~yd&>-Q0VUS`~_Pn zt+se}yp;Y#!i$2}jED;`kyZDW7UkpKT@gp!^u=3%Ck04|25NuYD%m7PBu1FV%*3M% zQ))&0RqpNld;80A&}GfVR|NX%$9tkVwxXB9{{Pi((I zEcdI{_LBxLl6B$p9BFbcU4%!5aAb)bomiTdFp5@&{iJ-uvREF)hi~u_oqw#G^1mQ$ zmUlU*`Lk##IWc79(`*D>DU2ar%TM=t)R>Atp*}K1vZw@9WQBE}An9N}=j2(YaMuFg z_3DUaF!o6K;$80u#~pYSGe|SaPhHA|zM>TXq`E8*hMTfT$_Z{6bZs7zfpUoG>iSBC zI8HoZlU$kZs9Hi5ns&$loDpA@$4?zA;CK4>FCvEG;RF8#T;=kXWTghjhG|esTSuiW&C2AdOr)d^B%zkQvzb~Jn^dK~M7YreLa6Y33f?cdH_y9v1`>A~ z{yvtSO*NkN#rp& z=1z(k1NG$Ge}3~*um@wDMinZO8eH(AyrvSX6k2Fr8kaw(06~P4*`s2_az7bX*xoYC zVa;QO)ebji-&9uBYB_RtfL#NE$;6UO^zFJ}V*6El^)jWyW>{FIWq{b8foSTJvsddT zB(&>wVO1SMPds$}tg%L>o||_4d+MFip6B3Hu~50pc()#4XJv`_9x%tR`f%Tjd@)1Z zG8?Clk&APt~%Vg zuS#gmE&u5zKFYiKY!gHrg+AK}rQ8gZbZE6W^V+&E;)p9UDsD==M3v}_4wi=tX%n6v z{U&ZI?Qb&RUYPe8VH`QqKzxw@Z(EsNJ#3-32!iz9MJW{z-PkizoyWyNOaxf1&%W@K z;ZP4|syzU&k(`6hzMVyV*dqb=C0+R6yMcUeoh?UUKfUWM%3bobuP66DnRMh<>C#1Z z>6@LhKWVLfhZ*myZ75pSF>mD@65T?zAn>`xbSlTX2l_Ymj1>a z8}yBvKW!p1S!`TuaF{b5{s}WXknA+cdnUVgMI3JkHh_AYyv*c%*to9Nxu|NkVzDsu z*#8698im3X|J^*xz= zyyJS_mKALUgnt3|RKdi$=#dcl9B=zcSGg1$7`fAoECgkaYW6AdYdmu)3L=tj@8~;o zw)|jIPtD}wfc%S2vgM=BaRbk}5@ht1UMS_6+&ZV`*%i>>T>63TFU<-vi~Y!SyM-V@ zXAcumkj2KGO>g?InOhFmt{uNBuUc@KOWQ47as1m)GsP^+J*#aeT^+{Jx&cSxZBaf7 zZb0>m4I9GJ%U9W5L)Ru9G&i)^A}+pZGOP)2KiwfQrM;4|d=cEIo$X>deBVf$ z7+SnD)Nr1Asi@@oKNv=fbj;mr==8&@;`!Ij(}^Zuk9q`j;_;NF)LFa=c%S)+ZNGUu zF!-}V8*_+9hD?J8G1o%k_aXt5;5#Sox&TU8f>?~PI<7?E>e}Q04km z+bS#^oOk+`;OkmBZDam^2k<8+S!W3T-Pnn{7s1g#@LuEt&05br>_^}| z`zCvqfN3 zH7F^z7Vz5X*Z8B|BoEweQsy1^qy6lN8WCY;Wj1_d246nzpm$Q{pCW|(V`U1V_@p#5 zsY=dgV*rE^nE1vg4j$lnO0iVgiJvZM*T0z@1#-q!SmAA1GksR*FzfC6+Dt31v>b+F zO`2aMQ!lDif5}aSyD$@BTJ%nkDA=9pZ8y!?7srS?ACX(g0RO3n-WLq%T(+-Tp7zYf zgzz>g-P>VI{&*P^T65K0V$hpNqN+5n&pi$dG&>Cx9vgN5gNaSs_y(PNTf_|=40N$l&=J6Ny6guS&U@Xkp{WlotCbr$b^ z>0#ytlpwp18oB?Il3?lKWE`cVRhJ~vr=#|tBi^~ic;XmH9&89e+@MZ0MR5!qEbF_* zGujH&#*JDpGlOr~r=SBJ=a)tg!j%NKNSeS?t?Zrl!0&4r=v7xwN1MMOzkZjug7ZpO z%5_mu7%qh|{8OrdC-H{lW@?px$I6mp_PuW3kQI@ly}v=}4E)&h3x&s6^C6i(y3f_2 zqIpW>{Pp|5nAs72VC48%*!ZLi<`ZADird<5=9zb^Z|@-&ix5#{+jAL@-Z28@RF&tx zejM+~^@(Nrjjcl(gN{i|VHM-N*Pc+Spx*{w%lpz&@JQDWj8+BW8lKf}$pbS3YA!tf z8H75Y0p$xd_$0TxW}!m#Li<7NyaSa>L`EAq%OzT@!70q5c?XxgTyWNBdtwu&-|L># z!{B&W5v+YKyEmJcPUxhWd)jSF|4N}Nx(7@4=e2bwGz;u|$x5A$g(vw<5KTCm5ZNp~ zCjS)QAM^scP54&VX-4(#PRAXKq0eCceYE@gnBe-`#GmfD{%7gA*^kP-j6fq^a^ync zFAygq;g(*HYJ*XaVq@$7s4v-%T)M!g_=PyrDr^Jb7E`I|wl(rBWki^sycouxDYLRV z?5Zzc(^z;uo6FLC-Lf*Rrmvs&Aw5X+_&{a&3ITqi)94S$vu<`y4!~@}tJZx&beu#uOHi@$P)0TL)GnsqnHQn4&F$tz-S@Jc`U3q4oQHh(hIWgWYFIZ=X4} zVds*0z|5unGah0Ku@j{?6L^XrRm`N0o&4JY2$}T}Qo1(mAsA3%jq{*stYh7+E40Y9 zmC3A+_I=QCE_ZsUbEvAy` z(s0gG4Je6U;`kWjM$>iu*E$28?e6RvI;Ri$G&~tiSbeso3 zKk_YOoyQ6o6L}82_I0%Lt~HN-7mc4O00sX>fOv^!Ei7PF>7MD@dzV^MM0s1QQ-|?E zhF@q?|0fk7@T6WpB~|)e!%^}kX@-d?|4a&JvGh#>0rFw`xIMGAVr;fm{^v! zOKxslY=sN(emyq!57?iP@$<<;Syy$}sALg2|!7AA#|}0#*W= zB$Aq1HD8qh>krrEx2>cWW(46wAc!|uZ!q?-XSMj%} zG4VLpQ@Cek*W-`}B5w4)Md5b(vL-)~+E;)qwyye>f@<{H#yJLR@36jH`GRcePAC z3dkt%6EOYfC!nlosrXNSVpJS94iubf1xKcqw*y$U7xlsnJ<#&MD5UzpV;W#2W#w7~ zieQ0>rK*v`b9zC_fdUCwIJ!36D@UYNhp(Ys5Q_|xiCQB=^8{(_R#U1`S#823_8m{+ z)2{@}z2|RbFD*JMta1LSZ1Wup+<1Q*%Rs5I1nebt1phn9kyD(V|iZ}-AR5OVM!sdWMIkTi% zpLKRQ^LKhZHCr7NN-T(^0$c(9StnHoq*{v*R&v~#Oll%gr^D?h1a1^ARi4_VQRZL7 z%q~$Y&B>7(%T7tFXz~)8BVHv--!2jR*0!!3GS|-x*lxp-)|UFL5ew?Wbi;qA8m;tg z>#4M+nGT|}Q6}OH;o0;3xFW|X(d~K@WGbw!99hYl#-0#V$hM?&eU20R$#9OzMwp z!8N!uxLa_S!EKPi-Ok+i^Stl(opb*5nl)>;RM&M?S9SNU{i0jJXInXu(HN#ufxo6! z!UdTudE@jPjj9$MBu(ld?5=O(_$@jePc8qaZ@(vHcoa48tTxx5GYd^z8GW% zp6Lv03u_nm}Fh02cmuSADHp2R-_RT=eCe3z8J`zu+i$qA6N2kU7Y zrn`mYl^z8`H^+oN(+N{{NDZ=Qd&w@8Zi-pyW*y)S#A5G|9)Ay}zxui=f0U(Z`(3}- z$*gGo2FYyO8aVZL=OYStT=m%A*f7p`?{I9Ur*o4-OhzYceb`74x%jIHu%=mwd9fGw zRivD%<}6)Nk_)qdFX;q%KmIgjhm&%qS}V36eU__fuv%>i@^JbNU)|DZOnXVj0cH1n zHTlAaBZM`1_l{7O@Wq>t$pEIg@6ZQxHdlh8B#{PA|$c2y(G~CzP)ycGp5PEQLHndtp-zb;J1ag;B?CjZs(*{HfEcqUY{qp?=0Ibv`<{ zT>$B-h)CTChvlfCiDc*dP>x&Kc6EO_T`Bz8eG<|xwM5RRIhv_9sP!|>bkc%n&SDJV z!0@WprPwCjB*#Sv^|)2hwkVC=k#xQC8F5WC^UR25dK-}IIPw|C%Sb%jT<6w?F0T9y z^hZ{`pseFdzu+W3GN=+vC(B*6?oP0x=V{vWW1ywo{6{tKv3I^rR$u{j=Vp6IW$2pd zrT~NG817fgg3`qURR~zU=wJ?u&@|B{1yrGs)JwBUTN1I4l6KeX36x04WOlWDG6M`^ z485e$uRecs{2P2RH0qq!^|F`QZExdQNW1IVj6VQ?dFJ})fw|XVRd!Pl zxL$e>B^|zo$+qw*PK)n}lpC9F&|H_s`jcFlz2>4H6d9Gls7a{sqEZz7+U=ltfyuzx zN{7gW#H5wDy`V^NtmF5y%eFy&zDrEf4ANH18tj?AE;$SC!9NUd80XaPx#lA-zLma` zT9;e9WEwnK+Q~G3cJE^-1dOpnUb4Hg-(Yx{9^YE-pS+_Itj$DYoyV_nnB|#`Xy};a zMwg99-jT#1vf=pi9NXBwO?=ZagLBiqpQ;@>l9W|Oy0?sKO2+D$Wd+&~>mimz0HuBx zHJGz8|IqVu%rZx=ojx*hPrDSB%V5%q^4Rj`3xy6$=h}Mfyc%Y;bqe2wA%i&wx`!P& zu8WaRB$}@~E3Dk)T7GUC5dOWT{xZG@Ommve*ml$WeAWrH+;sWCC}N~M7{`68s|8wG zBY1jbYS}e#unTh_QnXO9j~A(Vb819R@ZdL7m*;GV&%$T>!phJt?2wK8S_jlwi-ljf zApaGkOLQTzUz$?1^RE38RGvu`kH_A)wnt%Xk9XxoruW?6K5W~19PdbT zu)ERM*;BvDja1#u?zfzt`o3iTM=IYh1rQafaSTE6#e{y<5~EO9dv>)u=sbCQPS+=J z?Ng|1m7TUj|N*1&Y$d$FZ^BiF4fC%bwqhpU5Y2V49IIE;J>HD5efMH8Wixwmu8 zjdz&t7dmKOxthD4cAj)u_uTyD#B`mpk?+Lae>H+ZT|-7JlEy+2H~skYBu-o6@+q?hY(7P?kZ1wD_zt6I%hfC|R7KdZM)`&t88E6Ua# z&I4`HvoKLVLpekXygx(Q>f^h#yI`gA0h;2CtGxF!uddFsB^utyxK!=D`#Op$Nm|TD z>8?mEd}02JIcqL$VEU6mrnz4lvwKdsY&rQ}XTRf?Kay$KGshNtLh1%eoZ>`qHuMqNc)DEV+OfWa*TD$%?PxVt6r?(!8UZQZxhw=yW z9jQ+^DLD|Q)IWvNC!(V|tVZ6-T}ZrC1{Kz%$+kz5TaUt5Jx^78-|w2i_#dDCB3^k% z&km$Fx;E=x_j&Yyh>-?3DIlV#ZXnt%^ToBXrUiNInUsuD-B$_Y6`y&qw@Lj6OvzS~ zd@6hizG8UytS&lz9|vmOMbx5E!>l8>cJ#)GX|TQ-StL6IClQFq$D~QE^*H|jg{@6jErEVuo zqt7HI!}jjWdCH!?DhH3CCAL56@W-#xHbNabQgCcmlYc1ANmOu9 zq^|IaLqyFAgCA|Fc5_cs6&pw*s`w(yWt0ltSzCPg<@lQ*<4J8)v5ZCMrhxR_krx9G zzNXc^?#4&ifjUP6+!pVgyAKNs6%Xtc=6z|{u(Y?MJihAcSWE)(&ap$Wh*P0nZROW& zPlN@8V}Jwi#zZ-(U-NUAS6yEdxT-MacCPS6f9(-SfC~Wx%M_I+9o=RQ?<0dvma|GH z#|6(9F3CfdFX}<&yjbVY2OKY;&V(1}(;n5Lg_EV<3G0i>d^)T03P!~r^aw+%b`4qBOJh@S2H*xf}=)FhO7%O8OJNotj(<_(Xg0R=&j&Ow zcI25;!uuJ-C2U-q$E%|S` z`He}$dnM@bsNB^3TQ{zgQ~Aq*CBpL*90LINh`T)5-{}{Yt>)z`rF3@yq?AvjnwXhZ z^H_6rv3at!)*%;()FGF6!d8l>UL_k}dXC^g&m#mwmy9rnj6YehV@Bag;s$w3$7p_d z-7NG7>%6u`mB^}0?^K>_0;T7w>+v?7WWa-N<8QT@KC+7>GAPj~pHmE^RwNB5a5%Md z_*AAk9E-#?7P+JORLE^oeg=7vtNY8c_VV|IMD{^r2~;WUxwa2zS=C;+Tps8*!mS-y z)?NC1mM;AAbN6hoL--7OoF2Red79hwclQOYKA$TXy$VHMNrwytklY=7N%uicy%n5r z+pJhFU%rl}*L3uXR+ZbIAO7?&0Cu}<)leIS_A*7Psy!&HR9yTs`|=0dwLHfEo*(%Kx+m$S zb(ImLFRyHQS6|-+D2La5SowRdmnjNUUoy(97EKihAE;U=EneY3vltV3 zGTR@ziqkjn(P-4GDSKEz;d(Sks>^uzBodi?U5p(i8RJiax3q{{d?yo^O{eWhkz!~1 z=J$kI`Q_R{h$kIj_tfZIs6q>qh<8!nOJvQUqM*vqOXaqDRwtFz+!HtEWf=sGD)yyg zgokwYEGk??`n$~VOY?gkF7;yz5n5=#I2MxXzA)cWcKnpB$v8Po zRj`OO(Tvm3{@qZLrfHde3{_nhRp+9gAvOFiP~}4R12C=(NDO-VgLiJ%OA(lkZHOaJoD!t!_apn7p@Ge;ofTug zcG5aRw4VqCKkG7&MIvmiq`zSYC;g8dz4js6Zhb}dWe!Oeo?u$XG60vp)}Ym>lmR+r zHt<%x$Pqx%NF1V?WG@wG%AB4{SJ5?HpJd#Uk~Up=@?y3bc%#R!-Z&qcdiRu!f*-`{ciMyIIE?Wj!wotrNZ+L#uL|5! z`|GD3y<8VkLF}2*3^I*yha~DcMvZ=z+Ff|o-V~d3J?Lbfb2lhF07CS7{(mkM4|#F9*aLaYoo4yQ?_+{> zcfgg1Tv(DtEibk3gfa&MN6>qj_j@KTV(5JWixn;UFUP_2E?7nu;5BQ3Kzr#8(r>Bj zbeB>N90Ib~T;YZ-Y|Yp4ly>=p!LbgfQ6 zQ9cW+21nceuT{vXzVCseWjQ@ovCY(-5qa5TS@Pu4)$~>l8ZF$(hh`!}p47xREOirf zFElZX9#uAVnD1*a1-OTFXuOhkOAK1bTl;Y}ms|cQ81=8bZIkz>cG3!nt&ZHT zCg(C?Anm8x0}hX$s_RbeVT%{l`etrL9R{Ddbllsd(FbU}%E&h^a}N=*C(fmy8)+0g zk=9kVdnxfEz&8@thA*=Q<>wXWvBQ9oSBtOAf)5uk-Y-K;WSTA3l3Zk8tor=}7|Z>; zOe*}a#JICpxtzjHz1i7r5HaJE;@m|Vx@_Ka< zp}dnCFDwr0Vk@|Ru2MS<4A>}I^W3}aie61!n2UbhZRc0D`1l)=T9*WRUN&uv!h1xh zyBH#0@^>=1hbG$!YMySPINgpb6)Ff{EVW7Iv|kSU7aoO?5cbK9)`n0%+L)|#kdIXz zF20+0e5bqolzk-CMY!d$R#y;F%Q37i$PV#a3;ne<(13ZAOM}Sac_aEB{nO%s&DBBw z$v1PvzZD*IJ`o2TdTsCLo-8lUnAr>23@03(YDWk5#tUG-Sv{GfXN4eUM$8Z^P}qT$ zTlKu9K&i-RGs zEF-{AmSqoj6cwlWYlw3g?u$E>MOjz3n=r%*GAGw;!6r8oTKkS21&F)Tq`=`{Y)%E- zI&*8X>dq*}KJfx$3magaXnTE+Ts{rQLpVJ%zA?t6mPcTRF5!4T@wpv%L)^e<-fYF{ z-jzOhT@lP##X>ksc$|4p=n8UMqC`YR7_JyaW41xR=-l;y{d|QIRn(m;!AqA8%l0eY zlecz#W>6m4tKsEb;^>1ZB4ScVPayO77Di?gk9y|Hr%68RiAo*K4Z?R<=gUl=Efv10 zyl6!h<#s6NBWbFRF5^z*0c0*~DwB=h*|Nbz-|A8$()aF~zmHNf7->Z0 zOz~?-N?LZ4Z?&arW@HcZCyeRZEv!I$MbUn4%Ol@g%bnC~y~Yd~ujRK?eJKcQ!P&?4 zYCqCdSf3TWXk28Cdm9UWDEtJ|*%7{9)(BL{;&v_tHaY?t-J9(9HwXMbx0nEDXx8hM z$vuxR7uwaMDJD_aVeT6Zlj%Osa!+%5M}u|QH-#NMPrLp(VNWL=9_?toW5Vq%C);Ko zFPDv=fb_f7E2uY0@LnS1-02&$8`V3*5;{O`|DeMt7HRA_;lWSe(CZ27g`O*M&0-yn ziusfskAPQUI$PW>eJMYED#{Ge#w`0lRMBUE#|2#kBfuLYOXQ?Kdc7hi&V=*N>eJ@J z3t!I{leW}#@?ZJV>a@_?H(7apQocnuJG5UQb~f8-B7JD($WQi}&5-5k(&m5d(q`Ld z-VkMtF~&@-W#9O4nI>7fH^*n(yHoClB*0~KyywkeG3KN&w}vyR^_=(QbmRGb=Dq3) zu4De;pT0$GE}20*BJt}(8~WC8I$$mADXI1+51x7AX*NzEFTDcZ!Q1q`mPu`23cSo{ zU2Og`S%#UU#^UC9SadGAB8|l@qL&-Ru9#fB$V@NqvofcxSfnWThDVE4_XpV3G>+J) zOHOe$%!i2^WY<=RLh0e^WlY|R3`L@b{^|s;#*@bnbr{~629rD&K;77J3KR<1F1uY$ zqXZAtZr?87I{}gqcc|Gf#o5$+sA3v=uhK`Sk6VhHc9A zM>ZUdmr8cY0e&4Hr5~l)JQ;ZHnzx~Xx;-{pKwm`Kh0wAF0CZb==g<{pQnXH4bH>3I zCC7jUUzZOukv+JRjL*TLcPtBcE*j&KtFN0()=N+P{YLIvy8CnE{MN+LYwikW!rt^Y zWa2&UYkvG|<47;CIG~(;Bob$(SLny7aX8>2(sdz1H49js?|}JnmTb4bnpIU=py&y zkVHCjpmS%{v8sRnc#o+AhqE=p?r&{~DXtY;35Tf`@V;AfSjm*k!4ya~f)8!5*JCPn zrhsM?KX^#ldXyil%r7&9F0yqrJ3VF1M~%`jUa?5bu#BZ6{~SAn-Ha@Nm#<09(DHja zoBBQ!--P!ZV~ErzX2K@mUKN-iK@Y& zSJr*UY{4KIsXFrmx`MV>yx4%J5Xq$=ck|K^HSg2$__jIWY+7|^p6Mhyvez{!3NfzV zbsy=p&a-}^L4?k&UT;8Q1$&Eo?HJi-q;i^D;rl(_!uGtkb$?VyFQ{*?#9*jgJD9L* zes4N(9`W+DzU#XgbH+`hH$MplE#8GShNUW|N>l=b%n*&U!0)Ew0!n#{-teV~HakS< zaJ=fmT`nmu6|K*gwyt`5KM+uAvD29RlPbN<%RyvVjY&waF&euElF&A1ZPL%$9K@eT zM_4q|o*ACj$GJOfxMcVRL%oOh_ODfanQ*CE6Kk30K0czmztw~Oqj0gVhY9jci?xy&S&V1h3g33uEhm071JykyRrH^O=adQUS1-iOLsU$X8dO*a;b)o%vs z()^Kkhs-?>G>I>^m=59%aPJs1c69EpV_KH49O@i zJZ#bcFc=PA-}O(c@2J;Mq=a5|^l$;pu&eT&{<)JWvxxm!6@5#w=#%i#87Nb>OZN$^ zWKiYV-aOd-a^FjLL>nAl8xrH4y2*X0ZL4gGb>E1y<=R>WV)$gLe;lyp+2RGJXb2^B z9P+O?8B)srWb3;Vz6CzSsCDT|LHc~O5PX=er~byww@$75zJ>T+u{?CW^|sAT$6>Fa z$Yl9jvqf2*&7a~NZ7!u5?X+S_ejb08^5tg2>ls8j=QRWkc0F^xFk(jaqcC#Xn`+HI z?4mGcyu4o#?XKo3g}1DAmY8hc`qzxhxT5DN`u_H%*fDz~rdD$@V3_3bqm^A+8dZ0@ zoO4=4P8N^q2(eXU?#9x671>)4gM1fL)Fl(D=POVo%o=ZHW9t^i`uQ+*kiWKE_vK-N z52$7Km%QJ9e}DM9u=!(-u06$s)=te>HLngu2tU%X6A{XvY;5}P4@xZ4XlLY+m;|VC zCuiMsh4_90$ciYpsr?8qI>XZYj5!aU&72O_a58e6){ z%oXpQ2|eoC@KD<#!%IHnkz&cS|GB3uZZr}nI9C3}hVH!f5#w1e>ZErqrTltbyU>m7 zV0)MAIem_e=ZU-Nm5!cQ#2#UM)ih$kM5zd@h8Ajj2lQPmNa zEvG<9V3I&<8y@)ZNse=qY0tE5-nFN7x-&5MRNr)`~gx1R?aa@)~b1plEUu z3T2H8L=VS0Kq&I$`wBC&@IuF-9&EfA1ZliU55=}o^JzY!DvoJJE4GNvY@8Unwcf7* z{LQ?L(Q)t1!a72u32ZOTj_BqrqNUe=$>Fou~X@}8Smp`fgWoQnyNA+z^2B+v?@`5X`hc^&f7i!-A)!bvU%IWfwDs8Mt2O6uR&1yo6A>-$rW8QP|;w`ks41&xFK`3;CY zT$vC-ByTjCY%KOR1znpJPF=E{Q1&UMt&D7Dmr!bO&opD60xL3|BWMZ$A@Eh6(IP3V zcnDplaaNg{5h7dU^gDu=f)OI-?($JYk`NrLas2uj-Z>r}D%PhRD_I_D=MygBuL(3F z8NDK92^)vB?MJA>p(&aXebvm=2Gj&xYBH{&jnrqc4~VuXxh`(6Nqg3UkM2LhvX(Q+ zWg6YsCW)kF8u8$~HrAPul4;C$V&{?J(fN`q(*o#rK{VT3)czpjDl7^0L6cf}U`>Ge zNI)AH9sR3@xC@q_&fc#2kBFVRP+7E$5|u<{n^+nFQX=^lTC#oZk@_0)odC+_f=&o4wAi!K7A%KNu?oTm>Ge+RKMmF7IZbmtIBGx6Zq%Fdy}1-cehp5-opcw2 z7H&f^|MR>@6%Tf8kC7L3`HDy!G89or*qZ=|@Qg|9X-rLt~Jf(?pH<9ocMTsqhPNbf472nOhy z^5o_$^f*g)q6^FVJ)0X^^%ua#;7{IS;kLFAd_9$4#A%&3_Fq3X2a+*B79;~c#YH-Q z7Z2_HP$=Y9f>~>Qx{$mP>!qs%wq}HUR{Rm3rcJ;VH^s^b8OfX)9h`h?Zfqy^QRIb3 z!4C#ru+yVty>u6W5_~WWxT!{ZyBh7Igm}V5NX2Eg$MVUQt>8iAuxWU+Or_^y{_i5u zS0aQ4ZxJB%!P@CNX($}4vOsEaIgH;V($J&?*iQv0C}`e0S~B|9rgHStn@b8i#C>}z z5z!Ws8LwQYX?u6n)hAhcw;OpET?R;Tl^X^i*QIWxoY3#%rZvm;9dXt6iV#`qabQ3I zy@j#_C@onHl#l>(Pq2ZOz(LaZUy#%kI^pu&%U~^M8TpqJw)npy5B(GI%9|Fs62{&7 ztFv#29Q&BN-y8!_Vg}xJ!V4`GfQKvyFAfDy@0=<)f!6OjsCj|!XzD@ z#$CsNPOy6VirNY$_hMd#XR)VN*3-K49{G(twHJo;kE_ykyUf(@fZ ze({h!5Ki2g$V`PDP1WxQ#a$JG!D1gc z=f<^?^WikvRsQ&&K_&APq*tUOyyoxUgAT#@=({b{rFF)SRgZ+|0$<}Q$cMGV|F;%h z$baFr3IvywtEdgfC&Gnd4Q6#fH2otdc^2P@fA+WyT|H`?J3~t`I&7qL8&J{K;|*y1 zRZLEeQ~x=+Q@8sd4@}VDN|>mzTM0LM<+4gMQvZRTtDdA%#g`a0E+76ab(|ksv|m2JtijDz+v(q62WH{&ZH0qNVC|cM6^GGHQ-gqvH=?s zba%8qo>^U%{NNJpSVyIZG5lXGz>UPemH#QmRAp|aq*uC*Vw&((Icz&&cXieTe>U}} zS)|Cq79{{iEw~{q*F2gZom6Od{Q(YB65Qs%}YDRjEn(ak3F?}wA*|KBL*;dAzNQ_aL( z2^RCkT8puI;qrBrxs?Kio2vn-(}RL#zu}Eq)QnG~p-PVR0MIm`@txyz(iD27l#Wgx z$)AhVMSj6ugr0Ch+JE5vzY`04F+Euc+$x0u+ACo)ONkWwVXIT{++RAjAQ8Ua)J)~0L|U|d*?w`M~@e#bDL|11|u3+`6b&U6#C(>hN< zo|<_F<*`uhtn`Yp?!BgjRhFsjz92 zfYS)nef~QQf7$t&GYDdWoOP|NMHMT1kv!eY>Zf1gRpho1>Y{Di5pbtfAz9!hEZeNn zrl7r9-yhc$iM)UDd%^T$p~Buc_x0Y30#xxBUmm)fV8dp?Tac~{=KH5m$1QlMrL})) zPWrz6kZ<=oW}qG=EhnC`ut82+(DxZc6{xm@lhE(VvKW>nb7N<{dPU^SE(}} z%F%>r()o|m7B4UEf(K<8kR1%JXhI zdEUy)aNZY9bLa&t&`&kYsOR1X8P=7?^xNvk^y3b_WM>CE%G@Y1K#sh7Q|5zo7M?bn z?SMW15#BXJ8C=aK{(<_BisV3N)q9ynRNrsaJt=dudvCjTwR74z)XmJMnE1YlI)+Uw zZ2sz~-!g5+>oUE$%IYfo_6V9~E3_40`#>?CH^>%7^c(cmPgn5goshNh0#=W%N`K~R z8DD{C0V8BMB!ne^&44TfnB>f+Jp=Fm9}STECfKBnW2uI?V~pfEk^Tg3`H@Q6DMU@Y z+01PQ{xKG9Q#LD||3$G{f;M|H`zxIkZR3F|htCY_dJg&x-P1Xz4fF6@SFrKzGF9;i z&|2~^H`;#yGoZLy8QAktr1Ib!)w_{;S|&}-qrPw+k?W>fM#$vAR)USuUjeHb?e^|m z53+kj6O{)a;=OKaF8Hwi1p{+I`od-TVP6e~f^lt=dADbs+d^=s(3A@_0Y*l{++!Lt z!ZI@-IQR-UIa92eJx1$ihI%6Grl2Re$4t%Mae6~>0?9+;;aUpP-{Rgl-l}|`1#mTa zoG;QPaX;s%{IRAV%>66vI4zxwX<(3c&mhE%9;8kcIMpVDwp>)s5`l!2TDtrj`|0ff zjDYAS9)xYaLf*qoP3#(BR;&z0`-CDM#a6_o*es1m6DyIml`v(UR1wG+E#|ZRmKon9 z!OP}da_cS_*|tOX%OEE8rN_oOal?QMXPiPMI<%6eVb#nKr>D7iBRBXTz5_lmLU!%? z#MgLXm0Lced_knd-E3vl3m_WY^kfMrTkOySaRr^!*r41e#t@bFs61EL!-Y@ZcuFO0Y))c6Ue zxow+HC?09xZyACmX<_S-KTlMYU_%lc9d09ysfh;6gT)zvC|?*Q#m+URbN3@Z$0;~? zbC-d@t!HCkPqtI#T=zA!M*3W(g1(`2 zNRr8CTCkOGSa(96MQ=2wzYWL%b38r0j6}qtz1EE?aEj*%|IGUBtgHLa;tRuZw6VWg z^j2zj7imN4PnkyAb_R%E@-A&2+Wxiz-g~^cpEoVh%~Y$nwA30eA|EF5J6^`iq9Lau zz$KfGBwszv4w~zAk2iltpN%(W1>BI@x3R>|0Xlhi!i6bhp;fyysAI{QqUrL@60#Ts z+E-+ZVGFLZnA5NR&Qb+m?9l&rb=!)ZLRWz%>t8g^sN@hJM*T_ro&dSm`W&=W!nE*7 z9>j~5)L@Wy(M}sm@;3zaASgHq5F{(u{dcI*MNjal#nH%g)jC|@R#``38Lj-uAls?q za^`xKP`9z|qgx;Ot3sz0XCBkHV(?Ef=};C&lyp+bG{)YGk4o5&e18># zs?n=%y*74Gp8AIAt`Rqk$blS1=0+?72)_E1wbrV;B0!u1dH1(t_zNs{R8)1^FEnNB z_6r(c(TXG*$|E6@gujo_;fY;Dj0-8cQVVK{&g^2%WT&4miMve+52<2ykbtR3=Nnn8 z>C_q^o?Simno~841OrsM>Q1{2~n~WaqyUDmvz0=sABJpVaokr1m$zwM-8j1XFm#R6o|yiK8zU!9NQaIEf1W z!+Hmxz4P=x$aID0yB!S`?$^b-T5mfdUE@E@8feAghZPJ~GRfdJe3t-%ebKgUtq2zw z)DS*&UGzqZ&HQNWc|>&lvFakro~!$6fJ1}aG{FW}LdA9j9^)4P27_9y3RJJ%=KUNOt*}g1xh9tKXDv(3?W^Qjgeoxr=09$Z{qi0U~#Wry8Jlg#E~D9CjWyV$$3nsEJ5Szho+e>oBO;fJ(X6xqJvi4!hz&ieKUM267|06sKLDre$j&KcvazoWprcsc^LI% z}IN18K!<$c(ZK8Z$oxp;t9U}TmR{62!ITt!~4eS+va z_6PGP(du&RmRsZHwM*FY3;7v~Vzcl*@23FI@fnnkkB`@Rppp?&Ooawy=M8Fd`j7{| z-lxAt=2eTUgh34r0K+AuLnt%k&Qc4G7R|&dYnizIuSF8i!Nsdoo_9D4vJ~}%=BYe^ zQ4g;AD+36%T4V7)^QuS+(&IY9)``)-Z!L!gZ|B?fX`B$X>TqRUBooBtr=8gnpNT`| zlfQO9JAczJvuHrht}kg!{nc8O5g6nWZCkTh?L`L+wu(|~D~g!0!UNKHU@(b|hT3wU zRNCJS(IHA0EPiK5XYyY38NE;&3f<#+up6Go5tB3IC4~DrUQ7uv;J;pe{WmCg$cXT% zgiQb~eZ)I!)MUB*r`RBYY+zaKFAa7Dd z8!w&sXwAqKzMqc2mv^+2721})9!7qnB{wySP2s0xG_;0Vv&OG$k7>wRN${cJjSfDp zR)tO4Sc|eI+gS$7%Oe9MFAJf-Gjk@{{!oJB({@%x^KN>;+e1!?nDvw3tIxc1M+xWb z97Toa>S$rz9u%RJZouApg>WwoIAS8_aUByrE#C300*{zKy3v=ic@BKiSa8h`4dvET z;N-}t&1x#vFpz2V|HnV_L^!{nRAwfKqe$Qqa88yq&LO2qKr6c zAG^;P#B4ORn8ff{iR{f^y}lSU(!&RNUx#=rz&dnj0wltF&FV&g*W(0SmTW~51as0P z2{6P|$$W|DhC#SFS&+@OVkdThu=2v7zXt@-kE7(C`%k9ZlULJGZ9m36Mx`O9^cZJd z`xYj^7To90QkB3ZrwNWmkJBln@RSUA9EW;JLtJ?P;b}q~me(KCQ1BdaE;E~0fpxs! ze<++6l{M0ai8*Z$kl{Y_4Jb(06`|S03liBUCeM6>3QL_RXP}{II{eosA5X9ujtGb0 z^1LYHa$3nW;tmcU=oY?1D}}Bhk=CaUDfmglaL(0x|0@*p z%JebQ%T)26SCBpsFDH~buq-IL0y~-_WJ2&`0@hJ>$ zE0i6aED{74u#k%-9CZ0L_{0mH02zfuz#g_=)!jj2#xgm0tOev+GrYzgO@VL>mj5(w z=%w>r&P`G=?~u_N<_6Bib?Z*RCdR4+>*@KgYuXfHI-k!B_(OW(o6x)S&{~#nXYAp~ zwH@r%9_C{_jJqt&`#<%RbuIZDRJNsZ?F|CQ%MTF;9IwBQFOP*@oUOdJEY-zHEqMn9 zYq)1}p|1XKp!4(+G-a(ws^uZ~W2@aurm?4;6DXSZnRQfGTdjLs>1*-NT2dr*yhn}D zwc4d>aXBwVaI&qehc$o8_+xz~VZSf|@Mj#E*e$@Yc2yVB!~XeGDf?d9S?FeAs1p2;Pkq_$e}p zKn2FDP?5XR|JSG19Z9gk`K(|XVa@Nj@}iIc3;qe;)%I_IVALIZGl-=yNLr#O|1~TEhsWzJwV6e@hiT z73n?Y?fVX+KdKxlNs;RyVg9!=c%Gzy^f$+F5}27*%Oi6sPCOI-7;-01wB-X`k(2Lp zf{l)0!1{CIuz-R!Rjl~wZtVZ8k@HjqqyoN={Jkc*3_TM7gOroVPLlr&4eI zY5Hj(usC?KYgx$#+~qUs1|w3>j*}J7BTMKif`ec^dFI~?pi}f%&&@-7UPrs<$sMZ9 zjbWbC$mK;iJAPqIJ(i=m=ZqU~0zgE3RM^-_Oc5OWbjjjP(ThS^CB^1Gg zlm*Li%opB_Lo$~P5M6xZG{usz(XP*2+_Z*D+^M(;u)9JhLalE{A@8;J7VizuYPk3U z)>%J53I3^#Q~=Lz4PIm$zPtyEGiw*tShe!+ss~K94KhI9H+euh%r?1u3L~}BMypmh zg^BYO`W*Y_V7A|+Ux$-Bk^Q8e z@pOk3-Y{JECQ~|lqi=J#Xxx5}-UJn6C)mJk%Ta<&%XfBBT3adPWfR5aq&#aL7Fub1 z0;);yA=Tk9OI@ZFHhjjUph>vhUQpfB0-@Pt-UJvg3A4;@#XGN-w7$aS+_>uafZ4!+ zDZ$DFSjTTnX!*rH?qJMk8aFf`wb*DGBSbV{3O)$5z^TX<;Nen3qZP{`MWSBdovp-7 z4rEc65vXFslDS zd+C{7mQ?c)-c)9hMpaf&aVk;)K>lAX_yfT^h*8$=E~dw(_6l<7SyOV0^5{&~55$QL zI+aRbd))TpdpHYRykuPESd_s($@Rb*xQ+Jk{~UwqxNT>9=>kjHSY#T8vSh zymVfSc0(;VbjO(s9#G&N=eLZJmS=sNc?+awfCNl=TPuR^8X}bsmAVhOT=wD;b-pk{ zD!jfdzA?|>#BuQVmLwsSp75Y6Ai|b##&I=Tf9_G?~z8D(jY{SScJZ)xC+OCbyEo6}c zt`c@z)_3@++-c{(A13KYPPnhx;Ps)4$9u?7JfYn=)kLbq7Yfl#?kho~_#G8}(Rnw~ zJVb}_nm^pJ|9N+RD-`gdNFge60W-T`WKt4=5hr3GSvmLk~5e~ML7&UiOIvAQ$)m$f88_1$sr?noLC=WA>J{Z#0s z*6m}hqDyh*M-jE#&(fbm@h#)UOlG(=q=SV+SkCy~AFT7%p_8Hw47jg*o!YKrW#{pm zYPVc$g<7604!px}Rod20`Ir_7DWB#+3c0JKyC`{GH5@P{*GFC8H2!OSBFCvkRi;sH zZss2r{0o@IY$t_9HTi(?WP0(X4Z+4a3}9>`tGG$rYvQ=?Sop+3ZkbGry5uQuXNzuTW-)21I8T2dN-kmfN*dSR&2@SJF!_4yWfyGcs|Q2J&O*vE9SS zqYr0O@aR|%H^_w*)OG1u#+#hoH&pFuk24wB@ldURYn@!p_h3kSdy;lNl{3}jju6Io z%gZ@x#h~$_NMk72jZ?$i@;w}xmxmonaAWl!BMY}JAOo!nCMJtpm%*4kD&~P?Eh&=F zFefa0XgiCVh*{a2v)H%Y`TR|X+YjC+^{pN|J4rXl?7Byp0&Yh<-ZAc|GUHv;5c)I} zwyr`C&uAD_cy^V;XSX(x`p{bu3Nv_Km9vZNE7qd-;?kL&7eD3~8?gaOk;9W8 zBb4dtKfZ-SWDC9R_GSCA2a=s0jD2(rrAA@BF2Cqykqv}XP`$GC<$Py(E9hm(Wsnhe zb1hx*xWFz&%hFQ*GjfYMO;^upwMad`Py`WnM(+?BhCk(UK6G#c@X`}(Bs$<$|NltJ zF4NeTXH4$q(s|KcdPnH@|lJQu@U{#mDoQvP5B9r55DL+ zuY$VUZ+Nby6r{=DiH|OWd8%j%~(NVtgf z%kT$Zm*^*`YO;)w6J{aANR<(S${_;f&ei+B!8}e{kp7z002H*eAsJ2{^Z$_b6;N$% zOWQS|KyfYZTA-8y#Y=H0rATotR=l`FOVOgm-GjRm+@0W_;1VP_0fPMLIrsFQ`~53x zy(?KOdEe}r*|VS7Gtcb);e|ov1CCA--VzgWGgEMA9Bng~+2Q@o)!@A9;WLFrz(o_X zxj7se7M{ zak0fOHqyWE*RO(c{Msb0O3@a}3_ytzSd*1pNqRM{5M$n8Q@0tq{D1dXPWi+Jz02O9@DfQH3d4%*xvvVfH+=NzDO^D$lLXnd~x24Bb*eN7yf5A0sM z)YYcldP(b9?d^QealW14L5i_vI}H7i%V^%dkJRzrpy9yN^A6FpH@ij$iBT`5@N4nv zvkt0J5ibG|unJIx<^WDLUc1$1o0fFsr+a~W`9~!%j!I0ym)mhCV)+A4r!-3n;b2Z48fC_eC| zkmNTmqjYOol#z<_whD-imPYq1$ud{Byv+`No0#kSKgnKe#T*K;p%wC+xU5Y zp@~^@V-d|gv%p*ffg=7$54l5?ZE|c%Ei7!Mv~fvVtxk%d1(D&gD2%DAyNBs)zcePbw$R z1}g73Go8d5Fk35JOR%VfsDleU4PE(ur5j*>wwPW*Zog;FPdU2GI%lbo;e3)8_A+Lz zBpMpL+Qq6ykY27(nsu(RoaZOI7-$r1Gwo{x6@(`Q*LZ8wU*}f7ib+1G9BW-`3*av< zPb#^K@f^e8UR&6148x(?xUG*qP`s)e)g4R%Ba^}|jq>4b&Gne8kmqi@C*}%*8Z+gv z-XZO>AREev+0~QD&9!257RxfVZ~2b3Y2h86 z-v_sv#m`yWNVA-*ptFCl=kFLo3CdUXc;Rh&Z2|Y37tXSIXYmiUd)U^Ph6|YLdZA26 zs1%o*xg~3mp!x$@iLRzK;&g0RIk6XQAg)a8^ z=A4D9-hrLh&zjE6F!IErA(bc0L#+16kYRm%YWZebsu_akTaT?~9RdPvP^`ly%-<^m zv-JNUL!|e5s0{2nXvcd^A z%{m2Q9T{T1HT#B7r|JE$Ow8xV37hCY!Q5}t37Tl32%V`rc~4gzAQQKx3RG2-clDcn z`eooJ<%4*Ej}$erdb@t6Y(mAEy zZBHBN&|3Z{PK{8M=U-;VM+RCEB`=DznQ(}gfu$~0l-*LsEy@Fa9{d&szx!fl+K*-CCi|6o6u zRoVoHu}A8a|DEozmhwk{fQhLPBfA0v$cmPim*&-5wbxU7#097LxEvjMdgMZp{D;(D zHith*>TTluu+I5ZG<xQGWC-7s#t1L zcyETzK8n#AxJyjBtQ?dl?ij2BaZ5u*i%Jxt*+bkQ4sZcJ&VyV!U^o5*09bg`C;Ev7>?{gp`s5VTEMJepfU6X` zvrW<*Cyk=LXlR`&p@Vi!sHpHn-blrSr@cxi&*45=0LAcmqsO3FTH~lrRruZxfg2x- z`beW0U*aG>`k&GABDX+P&%GaQM?dJJvzSUf?Lo$O{Ds+hiCmUsw6xG&3FZyl68 z7w3;pOU4|Go`kn8^sBl5) zdc?XQMD}U7MOa0`RmxfUtPnc~cp}aP)(nfQW>I6I#%{*N5CXMst@@xbZle?T% zqPXoT!`_oKD<&7GcgOJAAH3mov6&S~oeQCSdx&lv*2rzRb69_9<1TwBQc|e;P1E% zvfyqjT2~jf4Nw1GWZYU=r6K$SiuK5cX;g`@kXBhD);8Oz51A8Ymt5=?+pMcdS=3I+ zRXB=RO-)|Q(Enc0ar^RN%-oVklUtrnD$MJ498+f5!*l;Kg`*1xY_|(@^FcJ?egqnL zoYWniazR|na&EDquPH`dP42fkMsQL*>TK^PZXD1accql8O?7{h(|kQ3*q7Bg8JtF~ z5dT#UNQIqL?A7=-VrrvOHhe}mQ*q5!r^oBXsZSy%Q+s4=Z5_g#~;p2i@Y!nk6RBS8glMW>T^StJ7say>i^{SWTY{ zVtO2z3{+}%yqKu`F(QGpWWIadm2o$=j1mjGwcCGALE8G-bALru;{J`Z2Za>KTa4Ga z#b;yeD@e$3GlFXF)!C7R^%W~8a_@f0%U23YzA53ReU=B^P5NXY8x?zqCw`|MH%n)4 zp7c&y?d!QNt{j~B)!V5{RRC~2xv*+^aE%4ZmzZIQJ=8QpB|U{{D8V_+fhr72|#SA7ZAI>lKDUGVESsF=iRb2VuW)|HcT}tgssB z$B_87uqGIJ&gl0=%1rmlcD@|X)re79B0G=;Ecv^3rhb8`W-+=%;b_annO73)#()mh z5cGagwQc9%*XRhvf~V_!dA&E=*&z?IGxyGtKr>4B08E6^TjVVi(quk#6_bA_dbgWy zaB+{=_f+*=yzgI`NfV3CT-tAT2udfl^&sTOD3=;1^v#xTd5yuB;Fc!ga61RyqyDzT zc{4#W#+4dnt`XDrsgIn&DyHGQaHTib-Uwx>*-Ck8>O+DEYbe<^3D{xdTEQWg$+dX< zrYhUX9HS(1#HZ0fesRM)ew3^K)CHz!HgH@!ZS-Vv4Tt6iPe<&dNWjWkbl++V-l-QP zEkZy!r$#lc?@&PC0CI}eo;g_ZGBn=0naTe_CPsEFf-8g!pH~vfB=6d+3>;VA2l9Qt zLPaJ4O6;8_71{Rz9V6|YcPDF^Ea#)Gu3J-1v!3SP))_|6vATU7UnyLb@ym(#7=cW1-j2t`Sh%RDbeW~nIVPT0d{F4vT%3{~ht*zUA7 zIzp`;YBu?}NQDPnz29g!?GNS)A%_eVt|W}R5V5(TkD{>TRU@3C5bTEUGx-Y6#$Qh% zN#5yKQ!={I9v1$!jsnWSU#xvn;oAG+uk(GLNRH%lM1L*T>1^dhFWK+%d7u_xqEk#k zoS5BOzJf57rWWew5fO}(DbZU&yq#`q-eqF<6uV*#C= z@NBM_=AOK}ChZX7O+S@jvBn{#=!ymzCG=TSdVEK_WQz;pPZ8?^WMh30*Sp3>Jhx1F#?VCbMsiU=OVKwNK?@=Rjz(`L8kbvV7`=? zcRImdD?&;fH2a>f!bOnb;gwB=~4GM89c;43TiIC`tnMmF;B><>3vHk{5s_dI6}IXj}fnN6S0t$CY`nt9rmK$=f+yzdMS;B-G_J2k#}O z`T#L>Ax$2vn)CT&2?#B_r|vRSGVjTgyQ$As%Dy6;Ns)sJTYLJf4gKRH>fh6S=>D@X z(F1fqOcfj-Lhl+*Pc%%G!Uc)mrlYC+ntOy7zx;N?%yUl=T?$iLH(9)0xOJWBc;4K1 z4AFEEsqlWR{}*YRL*p;!ozP^)`h00Atv>sETHNZkl;^!q!>{eVA=o!S>Z)KxCYm|l zq{MH2ZzHg6gbGUgaE`-cm z9v-b3s1hj@QMjUV6+nEoSOPWf^@(j-(f#J`#*<;CkbI+`Ucb)*rjYuqjM%*a_4+rn zD#YGv&{T}OwIxT|kO+qkJY{^WLXOTFyZWWs^ZcajXvOh<~ z>z?t)WsD+|RO_OlNfQU>x=97A%1JXeaYwry_nZD7him{WUuagXX!ZHo%wJf>lL3S- zj(O@pgr@rP#yNNr^$S#a z*Z0{>XeXgkUJe$g^si3ki2u%f_tm|8W+LF!(K8LA4rs>1gqtLkLE#=Z>X^KTc8 zj1OzDwO@fJ^kFwBZl;;iljjzN4BkR$^C`E{)eT|~1o7h@Q>7v6?`vXV&j?L`@CUYB z{|0a=8CbDBdy_jn*-i#OOg1I~FMXUH>->jg-qnz<>Plg%Tq z%0+!+)mzUSTKU7ll^D*iUzW6a8m8{;(j1>gQ|4Y2jFf581eU4uP1+*npY`-c8Cc12 z(qW{1e*D+d}YQ5Fuw+vT=OY<7_&*zv45G-7dT7c@m!~F2EE0BQW3v{l7+~B=0KQCJHf^gtnr} z&E^x>CU^s%m z_W>xhbPFyJR^(}LqhYx=_;)oy})88l!NkIaN{-v&3xutx9 zt=@9lh4^f~;Pn%XR2qp) z&t;~9PBkf&feDWQI_9t}$A97B-ntc6*@lQ|^Rd6Bx13L{KChB3jLn24&o2S=LglaO zniOIFDMPU$mYyKbxrQGDPpGmnS#H6wX07Ev=hPsL|G+t$(Q8U?hEP5)z2+OH-Kicx zTKaIT?cw7S3F@BmIP9upRgj1rF{7V?=ReQ3N5;v0iecA(TSu*|Wk%Wb3H0Z{6qcca zU#23oEb6vprM`!RjOlxU2>!z2Jnv^hkqgx{O zAiK9Imldo#w-ZJ!rYaRQ5iW}3IO&CO(h*>J)fr#nxI~wB9sHNcqGMhP{nuD|0Xnx< zQyqx+;Pg2bFxA|HMwD(zLvl&^x4N(EcL2h&S5X+phrz-)v0QkMIi$Gb*3tKRkR7q5 z^o>G30-gH@Z#=4Axt6qRoL@fw2y~w(8f=ao@KUU824qV!OHC!bvg2Gi#{(el<)JgA zIL65OfXl)GME4&Azz$ff!Yvixt9qs5^FUekLNY>=YM5wf(NRtAuI-urZIgV@LZ(cK zUyV_MQpvNjz`aYVdQH3+{$3fHSmm`xKvB)ijZM-GshOVLK9`D%#QBM(wM-`i?>62l zr8I{v6zhSnoHX)qgc8(843SLtXWaleEmr!Te;M(zHl|@W_Dcz<=8$oM4HBhwq`I@> zbq@_1#(%lSYW79T<=Z5BB5O9rk@rp0z9yyH-TeTY+f$=8k>bVJz>mF;d{vw!)QjH_ zjFydmeFT=bE`Pq7+n=0^58HaAkERT~Cqd@eZQrlW>?-}UXdxr=Jr&>xae&decqT73 zCwaWZh*eGIc!XSKbDgXR zYVGI01aOC>h+TFd2x6jowlV%S2$@=x)0vjXHCu9-yj{7?$0rn}&%23yW)u4dFDz3M zQ9gH~WOot`ucG@)Qw2v#;xgTh!P<};dAB>EIUq`J@7!`bLl=YX^&feZW9CZj^x(e8 zXBj~f=jr!*)e?5jLHSA<3;g4_{=)ss&ihBQCXj2F3L;#|#*XRT>k6M4?5riGcc-CJ$fe<_LNSEU5&+$BkJv|Bmq~ZsJB!Mw6rm|_n;Q$ zMxI8v?F0_OD400|bhWf`x4q=)0J={_q};oEnTK6^MGkL|I8lWc`Lb_r2 z85m2qe3v!$>}dccT71o({tPep7%kVyGqA|Tr>l7Jh}3@2`gB=uRooB03#Yd1*)yH} zpiay?A*!;KndKDO1}4kQU^&0KN-~Gxp7LCuXq+icilBFvVq8_l_e>;+{!EU3VDMQv zd82A7^!esPZ*f%JSu-Y8KC`d#@HZ@AnzPv8g@st#B}$5ufLrg}mzy?yjA3RM{Um0<)RJs<;{9@2AIoAX1Jc{hUX$-PW{ozxp=D{ z9XYF`fvq{~DZre;ph{MmYKEjH@(d-9MdUO78-S;9EKEh4&otXbSlIZnFiC(9B$eGh zZ`V=q0mf1W&N$PshKGf;uT$RZFiN98RjWt_(gl*|#^FDa>|KG_d-%MxQBaPM_j%qP znsrp;i|$@LkZ($ zM3BL*mx=2^0MquoxUY`HAuNYTk0w}Gz*&Glt6-eTerS;c21z;1+A+UcF_+4X>cpHU z!OTH!nw={3c%dMeC+ni`5Ud9kO{Ljf0Su%J4W;j=x<6(z<~v3Hnh1x#N69qphpYHE zN(@TcO#6tcThPOtW~PuQxls|Nky5h6*HI0-#bmeEY(@-kCt5zk2Sa7Zl+M&) z`ZE{#X4`LXS4}A~mkkb_1JSe}XLelynQD&1f8Z-}!U1HCBu?>8H=5*`N5{B=o!`TrYb%hdhaq<3plP z?;kdtu?Mb6BzBWDmh8&F`MePI6|GJ2loxm>o(jzY8Va5mXsT4t;)qrMcbF(c-`qDG z1#GVx!J)btXHx2XF1}z%MU`+$^|w-EjNp_H6nnOUt|j+Yyl=!w@t>xkEGD=wbsw-V z+ShAnpnx22T?gFj`|2$~@I@y(F|Kx{Z;D4Y^Y9qHhIFBNzCANS-USBR8QTm)AL7+4 zpLmYi#pGPRK#O0qLg7;tUO(io5`9rGC*H0aE(!m_F){S+=vi&e?D_IN;e_G?lnMv1 zSofB>yYy>~C0^|@@Ovx_H|6`!48N^xsueB0l#_*A%z>d-+S^V<*i2%Ed54cU|6#`= zsmxkh@78vrRiZBmLS~wuei;$z`B6Iih zp4b;}!^NCVc)jmzjADXw0#FR)8rfU8MZ?^0u29q`x0(nE_W3(sLV)uy`fa@&>qr}U zMqvGC@fQHFb+&X$_sBx2@=3Kqc7|4B2G%#wvPJfJz6LTS*G)ck%0Z|Bp7UY6n$YM- zXhs^TyhQsiIE1cS?7Mfzi%PBr{-6Ha?YtwY6C!5v@VL&7DTH`G@@No&$2lPjs`1hY znT_Wmp5H%Z9ulQY>p9eJEDYzc48JdXo{#BKfwf4YVDJY2-_ACO6~ygI`egq_oW+Po zXZGt^iK#iuSF)dLBNAus2?Q24D834Su0~(;>RJU3w3?G)pD$N;(5~Q4(I2)fc`VF3 zj35hxkvV8!i^75?!}cmX9%U}ws!KS=Dt}F+Mk<^RAap6WG+tj^t8?Z~wyK)v6x!7Z zYhYwMpiDT)z&Zjj(|>8pnIT~i!`)r|YkfFwl(I*uxr zQ7rZ{X2tB-v_Jv|8ihi3?g_Gf{|})#^3uPDM&I$_MDqG(joCrzajn!y|CZ20YIZ6S9GU#Y>!x!Zfi9AM?|9 zY);XuUebzz*&Kl0hqE;yaHB$PV4Ig*{j0x5)cecDDxG@8+CRY@WQ zJ<{(eQMNgv3%O|+T(#^nynmBLMkm@pg`lc2C0V z>qp4Sb8=QNDp7XbJWZ`QiadXM&R6aEYCdw)%}vBVTxDc4fuE*Xj)eG+zKl%qxyZZL zNJ6m#j9Qm!`J(GNpMQ%Uw73R3MT@2GM@?+FTnhad)=iMY=!g&6uCZoAKaAYWe5>%n zyg~&S{?o+zz=fO{?v4zYs-@^tRR+z~GNqXnx>$0qA2ouItLO3u=nS1@u_|!$Tz)Yx z074EbR0W6sVH1cLv4US@KY=U74B|6$g{rqiMwVE2(m&&VABgSRGxfEKjX5>e6fN(m z3g7fQ_09a~leHHrqC?`5#TcsXX5UR1O(*gDi|B;3;`$=&-KbO&e{#HE6+OGt#MY*w zyx}r4edL6TPTu3uZeAj(k|6c+leg@mK?N+QAo8b76SYyYa@o8BsVFnEERt7_mFX1O zlEMGS_~4QNr@eOKp3be4s+t1aZow9MG@@z7Ik1D3YXicNH zY#Z(V&rhD|q5?;TH2Z*Xb`_ zx6*6*dI*a%47I*xVO+G>LYk{Y9Xhi3%YOcy0V-+TYzdqjSv zFQ)TNI2Ye$oH56`Jc~m#LQV4#3O> z2^>fwEi>d?^V@2cJa z5P`}-C{tk3t#Epuu&~O1WRm_9j?lfqnyyB0eR!-?69mhcnO!Na ze=v!rfABpG+q<{Amw>M-aY=G7->J4aE9LhcoO#zdn!}I1(FzKXCv~#Z94tE56i-jS zIvd#HNZTIuEaRnKo`s`IyNq3QjNAGy_IX>ac)1r~Nxt{L@p1Fj#+C`Yd{hM?NsYC= z!fuo|v35+yO8`ekO}Rdl1M|OEab=C3I!>Nn8eX0pVAZf07852NdDz($q|zv7R$PQs4!x@u8bt}1y|?&x6gSL zgZlQfIR3;FyF$Q=`HTFHi{3SjX1_k38ib6irvZmsY{QergHQ5M$m#HpQWm$OX#6-S z#To`nzxavpaj6`(=P5AZh*+3aj_j^Pm-o9=<+#Y#oAfC7^&h-gNeozFT$jNxVz0Y+ zl2^@_0QOs2o^bqaexh^8?jW=aQ7IZ;#v;EK3p31gn#_`_y^~TxaQ#_~(=%gjDagKk zb-C<(N_$)X+{V!`rs~E+h$9DmuwPI{pg;B_6;X7zjhxc{5LcP$0OT4$bKSHBOA_vB4U0$3-`_3g%Dm`2dTAiM zAOXH_%SIgq%nw){CD{~PE~b!^qmhZ;TV=@5NMw}g&)i`wIVw!Y^O>(FSt0lj_8XjP zeRt+&6w{c+?DmDU>uaiEJ-e73+oRa9u`BlNqdY+M2ur;xZPvA= zTkML}H8!7`k?^WTX=d6=NXJx4bdNli>tB-nl?!>(Vt&oss7G2jpvN4N?1YREC@Tns ziEaRMAcHSrfE02Gvo3{(%c&{$tOm;ah*X8sOwizE)^zcZ)RF1&$MG!IG-+${VhR%b zQRz`_in}VHIov@W9-i++E-Raxet!D>7SrJNAClA)fD`$~X-UC|g|92yH-gZ>&*noR z!>H~p^`w+cUflX^vf=2eZ{F|lP7-~L+WDvkp4i2-h0k}??+a$_t|kck<1V8qL7;jV zrYu7~iEogb?>o~{Hl_ymjSs}`_!3rxbnny_?Y@3c)E*pRw>+~>Coa{w_jucH6@7mb z(w~9!Lud@=GF`%J^1Ly=EjZ=x@%Qv#T=n8Qq3Dyp`pu27u_O)mLq}8?XqH6K45Qpr zm}`x@0UPqiZ&x|lu(ff+LNfbr(h84UNxdSFUWo;O5FcEjW4py%&i{v;7=lc*(C#q_ z#pbr8JJ7B^EZ!#06d-Z^R?a7$8;F;5*x{j+eXbvT?xRFlNo?rc;jqLXR0ozejdNUat@_1&zPU16}C%d{U%7ys4Sovpll# zC+Tv~`;NdwS#G)C~TUUGE(s zEBI1z8Ds#9m0g-JQ6c!?0Ph&g096Ay#_A}9s<PY-hk zS#3_oE?!-$6*O&NAK%`8=9N{}7X7ajkQ=tIalL(>u$~Z~ zuOb^SiXKTAc`WDp)?yS%ZYh*N&W(NWZZnEAIzP+C5%GhDT&M+{PB1JiE(DCS%TLda z?`-amopCS-QeWyK#M*2{T+23-7oik{x+ATR*)wdq`XiFDP8!*53eTt(vYC(ztF>lV zfqd_WEA#5N+d-+%XbQQa)IlbNAA!|Sm9Iqj^$}Z@M4r+)j zv`!Y}!MEWlIL-rYTNk!w3}sc$foR?By~#(-;U)SwYjv`XPEIVcI)Bq`44I~JDtH;9 zNR#-LXli#6MyKO*7F!R~F~A#B$4irCTUWELl*{cT6+~4gD2cVLyZ%s2MJgvE{yn!0 z$G6rWN`Fee1uP$$zo}I%czQEC! z!P$aKFH<#c7Fp^*Pd`9gkzFr-NZ)t<_xV|@;9-87Xglps!Nb3*)(fh>r~-`{tE##_ zs}b`$*b`ufXU`#cip~0|x2LPIf*ZMZhtIQ2bWw`SL#Y%HF|(dk7XG2R z$VP6P3b=a^(vdHt4i2wtQKECS+5#5Y-}=nly#WZhAxq6~G0ny7|K>E`G9rCWO~(8s z3)l>o7d7W1+SZ#TIEiph?@f%F>6>ojDnfg1ZCNQv;X?V^n88Fh6}d=zH+`^K#h0IC zFCs@xZYGq0$IW~;I#)g=qqPGEe|2;E53Y2ScYUb>JWmE$-^ALs1Qv*k6Ob!y_pX?E zwtYV_){byA?j(3y(~ak^UG_ldIRo_R8Y6MG5L=xlAz#))Hfo!&S0l0};6qAX_;|=u z0Ky_JKriEeJ8@yZldm6V^EHbhOrLLH1@8)BFf+!?K4t}b-m-!>l^@2!giK;#!T6@+ z800b01y|9?P$yH4hi$S>wZnG$%OUzM-w$;9lKUYD*uPh~*Pa5jUk%#;H(MuHyynib ziB?QLW1MG*kGfDlqIqhm_L1<3_!*nq@!F@B5dD3*Gn(K8Dlc)IwL&zUiIk64)lD2W zHdd3b>ln}Pg?W@*s)RsHtP7)^@QT=dhST@vKKJwYqobnKv9`$OPGrBYu`qvDu=<(e zVdZ+yK2IzRX`u&CoIlt!tq;&N*`LEeSEhY+q?VFdPDqCMR3`G8$^DHIQwvO z_;Y#J99FQ)yEk^~5qsCxVRbgSdvnLRg)X)Vz#(?`Q)Cf3_%-kED+@S84Jgv*Htrob z0sbOizl**nQ#L&FWzjsmPVZ#Hm8Y@jxMUPI7w;5FLMFrW?;oP80Pmkdrkz`>XL-}X z<}6^WJg3D-Iv!@k-~vU_c%2f8hT9F^Cn1RDLL&!MU#aYFCl3{X0XjNQX30TpJ8C;!2UxA3p0=Gq#IdF6z1r>JATXn?9n(uUkbp`< z^9se0S83>n*a;DPo$^UBi2i8njH-D?mJ@&W#DP-Y5B35v^_qj!={hltvL~)$Izsx3 z$~-CM{Rf|h(tp_u?4c7c{tWo^Z#^-vg3D3m>m4Hh5#M;qDB^Ri&9>X>r*CnUhsN={jJRfQr^aF;H^NT`Z}9l_U^FRZAd(gIn}?vid4-fyWjt>WFoC< z@~Y2jkxkXSIbIOiRBrjV(*G!54O-TQ6b5zC1lWqo79doQ1dTp53&iOO*EW=&(h@wn zd=K2xt#pvwEht`Rqvsp(YTG zX?S{K#^m*G>U7KD%nqb#H1;^ygenQ^R?1(%KJ?7JfPK;ANBG_AY)`vj*zJU|1Lr^q;S7c=_PvQ2rYQUpL7#9Y z7^HtT;@%rNpdf$ml?kZ1XaAS(|C*<0rHjr3H#$b6@$}9nk*dRSI0-PyYe^EO(f0#{ zh#**m54YEOe@4(O$*5A9tBX9os?>w=<3LzvSC7;0TK7;EAqJ1flx0s zD0cE<$zzJgq&B`Z-6M~e(zb~SHs5|yF2%k3Q` zO=co{{{4?V)fd~QuseH}0Bx7+1oj%fIPg);9Be<<;F!c0fn5yB0>#wftzwtDMN1d3 zqaLYay0m;yw+km zhP;QO6J{B?W;aumx0(Iec^@Az{vWN{x`Ic0rw^vmV2nET$4Q{~z3ku-v`<>Ic{>n( zE2qPwLGV#Rj&BG?i@mkYR=RpU&VBP?plBXMO~zttV%+!h$>%%U`}+TDgb_f++LQk5DIL_r*4Zd`>ri)&zvY?GBasm)H93xgd3w z^QK@}ZIUZHSblo4W#mb;&TJAU^NB*=Hp;(L-t;im)@o=DnJ%MVFz$072L%ZXEo|$_ zyH495&f+*OSz%>Cj#A@l;YN~ow(ToTf{rdP{v`vLKO49(tpwSabDRy_M4Xfp9?{gh zR12MHN%^;0Vv3y1hwq%re}9i1Y;(O-HT1$gkF~(S*pp(OSHN)xvNb|14{zH~!~Ci? z{k-oG)&f?nl6(LDVtgcU-*!0VNv8yEq2a;f9W|LnyhUXv%lva?p~)om;)sY3i_c}u zy|4`>IcS?I{gem;*og50@KvZLT~MD!&87!L(fGf8N0YTI0HlIh~1|QyGV6c_zFB^7He_F=A~7C{JsS}KuA!w8b3@_N;baZD>Y7+9W+zrCArn9ZALK?`@XBL)5S@SH+%d43h(uZc9j zSX-TD(JT75xlYWH6IfT->ddADiW10*HeykqH8LyI@Lxp_fA&S$aWnfGoIfE=*Je)Q zfA%IoaP}C_$*z~8>LEZV`1har?=2mWP^Ll2$5BS!b%F()3Ve3OwB5t_3X=q5k!?EF z;U?N*|FdZeaNXukmFx=PLwQBc7gRj|AO)^sz7m^zz!BoL#^6ZXG% zw~Vz7?mL*2@x`MB2x-N_ilobvxgJVUtRo|mSlh?`zcvYHN!yk5@MnLqUU>qOnID~XBM!G+HMuf=by{DAbbe33NYp!bnEJoM?#vPW_tW&G;jO_ z$7JIqshlZ2FLA<63uC(j|3Ofr@?$VLTjA8y8#>2pWUejN_BE0iA^lsZs$mmp{Ai== z+{PKppAaSnI)BUIrw+w>;}b`d570obS*u~koX!TBg#Kj-=@Ynh+|7uiloC9!k4?(8 zOQb&uD^mLZ-1Fx|sk-Icz%|iQWK6~cSd?(ytHm&u4-~|SMhEC*#mQ+Q;;}b&tjj9# zfxXi2?=D`bCnlW>lPSRKKF08%{O_2jS-`vcj5S^y`>I9;g;Ia1>22I-VW2=!KR4|w z4IF)Y-i_$&L>1t{WDQ~>Cb{=6(_+|DOXeyv6o$fw2S#qT|Jw+qklh!?uPK-TLX_{N zP!lnY9Zep>+TZP3B18KOY3S%U3z(B6E1WD5c!i4Z+De|8;Fm~Xm}p_qTRC_6m2w%` zPY3HW)K4?Hm;P0bQ$FTvrsE}|sJ1lw9SGHLD<@aP`faP>$gT@WaMtZTc!G=rh5*l0 zWT1=O9FMh`dMGQ>0t)4{(EIi%sNSTba->a}UTNwUkB)(I`rh~Y9nU)xZu^|Ff~#11 z;cDDDPC8G>)&=pS+oYWA{`Ub>`A*fKpE^d3?B-D85LMuhuil^p)96j2qz!l%G#@8msbMRr3O`_WLe$h%} z|C&*h3MQjlViNC6OfMkVvuamG3GYj}Xg(E(k6@0-n3WbXRPTlkoL$p2dMKhKsUJ2G zmY2aLnkhc-TG@Ex2~mj=3lWEb%DVK6L?S8~WWUuVL%-h-uTPD4N0MZ8b6|cJs7ma% z5tnz(jSogk^l#B^bhFd99Ou6SIgsuN|*tHiX^Q>G!Rp_7yD zmgw$&dTi!P`B!1;wfWTtayW7(k|nfhZk-J5o$yE2W533u*MzuAvNn*9wCMirWB&4VTs~K@ zSw%z&2w77LyEp7Big7H^vS48fz{2ltlju}T@qkk-Ew(uy2DUcM%t!)w=ew5;1LnM9 z5(Az{_&8HQ$mTBh1!FPDD=YTi`%fs|?~?4$X5MnIktRg+is=Knw|OZ?oD%nTurCE@ z`Pu6=db^w3`qfA-A%-;eI@32b!K~ntHFHchOB9yOg6Sv+y-$MP(aOLpZ#lE=%tG-$ zejM3q9$`v%n*Y{>Y9e^pi#td0LvV?8c`Y7EB07H}20!UDh;SCGq3Emj#Nmh%aQ(s5 zc9WR!=^YG&h@if|UYp6&P!d09dNiFNyAIi!1dm6A?zy%uHnpwq1jPT)LE8$TK|MZ7 z*Yob33q$RX`^bG9fZ@DeiJTZLsy34N1exnF%{>G1Vl>1v9kn!g@{oX=OPFAC|FPTX z?6<;sQ56}?%*d5Pip(4*mD-$%OzvQj#DSNOc!P`}Aac2->(rN_a`Z)VCC7ufJliop zC$Qjv6^n?fD58=nWqnzfx+a$ zBwI5}H`PexQ@R$^*{XMz2KBD41{+5gj(~F&^9!A}$hwCio?Uf)O0po?QcO`dnC5Gb zv(Ml@eySKqb3)DDXG}4#bY-ByyqhP@}dAoD* z(!QTNKYkgOpf+AHz59#x(wEN-K&pL0yir-qxd~xH`vq2efA=cD=*wy9+!&Fde^liOz~_ z^*Gs>I_IKbOtYakrbM=bbrJUbNtQd<>tF|XL+IJ&d4c^qc-F&jOLp&g0MMu z?sle&bZ#cd39Fydl}YU`om74SAm=MGIlbe-NrP+R!`Uv^i(vrNY)k5`QP~)KH+VC&4@ix zGqd2+F7QmlZW1qvT)3zZorS>CQkRwsJ+N4-Hg7Af?~Ccyny10drvF*h)Ny~Et7k+F zWdknc>bMYz3OIchYRce4@e+^8>&2#XJJ%yyQjAuCo_L1fQy}J zo>XQgOL8X@8v7si#K`kS;uhARSM!oeYk85h0tK%Q5CZg{2}3_#=;~{XqiyDIIV(|C zj0`Z;6z#W7insg2AGU=jku4V)f^w1c8p}O9lYhJAgb8onRCCQA&OHQS{ldCVkrH|p z2dNExO6gG-&*(0ugRP_&)NHV_MXgSowX zZ|0n$apJ`ZQI)c@J*(m4nnKD5X&L|ra6>zMXFHP?rwC)^0xg zY%2?0CLHFfV>fX}t;u%}_HE1mBI%oaR%NZhmPlT`&s}QxtNDExA7V)ue*H@O^i+a= zV@dwo6EZG%Ap?G0Dx?2;Bdlf%dFFG{CI*9zjY{2b6_Y= zf;qw_aO~y(R(RU@1A)*<<-D&hNEfoB4<2EZa{z8U@{82;dSA|Y=cgA!J~aM4I0&D~ zqI9jBJCh$Vjuh`Qy<47Aob~cpYxeRqnX;f@SBYy1yx6*ac+YaUZMC$Lx_&TlQxfgY zdY`)ng|Fq-pTcU-ITh*V)Gn|3HMzF5XpWU_K@GRDJu@I(7}kgJ#USE6v@M=Y$J;7? zgC4;FUXSaoo)3zM>rWSfdljHQv$uZjk0oWntEFTc+sbr)XMN>*?S0fg{{K6|rg3(X zF|>nY$c_H|JEdD+NiL{_@Y}N6tcsgS2T!~GZd$3FznAcP51W6JG&}+zvdYz=tCu}0eoc4*pU8;3JnL?ufg*w^gUIYHt&%40aiK1$?QNNEagez zOPjlFkMF8~%i6tx5MD0H>8oz-6CQ|s^Zr$m^RBzxs}ieLzjc(@j(5*M)hYyEw2lcv z{r`R*P)hi6Bg#jM$NOy%{qu!HQjnb9N|ku9qE1ntyhF3d6cF)08tAaoOjR} zqO8ARGs^T6@f@YDi7!zZ|2Fn;TTKh69V}C1q8j|APnaWKIz)T(=K^e6uPPu}3dm7z zhTlF`kp5f3YxrsCw)ZP<^zkmWQNC9-VoI36x!V~KOe1+q2%Mu2zEqmgtVV1^uJ*6! z8x@4x*~Q00AZ@QL9h~qlN$H&k39zvakI0BqpOI^S14{}T3l$c8HKsatcr|b=xnh*J zBh)7XT4>J37QNU71bjK%#t!%#{1`05|FiVJYoFg&nN!sn{UFdgP6PUyC?l$hikysC zzMeTo{oWgxk(npD^sk` zOnck(Z3>ZC_9uHUL+2u01igg7EJJfx(jZ!M{Mo;W%aK&4%)q@DcSP)k=3=amR4sNx zKf@o-hkRf3TH6TJnKgR^JpFyJo|eD3-Sj;_NKTUbESLaP=iNI+t$B94&YQ0!CQ@CL zQ0qZxSzcayqphayl$JaFLlbiBvA*d%H(^8$@$T1ehCuXFYEb5jE#I$Mj#d}6VO^sx z3<~aIaMt;3L$}tG<@*yclORu>czyr~(-M)IqE=N``aUs9C+nGCRo5yZ4?07hW8wfSU z`Io`~Cha!=EPC2K&Slo?W0h-*YkFh6g`oMJPSohg*`%k1_^F_5uBKq4-6vmQHuv+CB$0Is`032OH3r_s3NeUo`KQ zsD&&22xz?A3@2qKb!!~`&4^&ZmQEKi`$1HlrdRqpUP(VWjJ6q{-J0+HXLZ#-C;mgs zXXw^#S9RBKdu<+HT#>y2#QeOJ}J3#y^|vLpM|u~nijjd%CTW;}oF*FfyB`e|NWbK~cKBI*)d z)AS$aTN=yFl5s48(s#WwE9}R2bB?>yT2*GV^+yt$RJ`OKj90{-S|mY+1AUrZo&UTv zCUyJL9C0!FCp_~@t;Aiex=D60)6*yQTYddFD)Ptw2mUNF3w|Geh>HZW^$yJx^K0Q> z!tSW6ucIf;(F7-vYwM@1;C@`-%)8Z1R@2b+W>?vdmE-2DoiATsHHCau=q@O!6tJr9 zUOJvE>~Q(ScY*YHyH1G=LH^(Hxs|o|{7Lj%_-rsHb9|v3FU54vVFGq(D$ATF>3$$9ik%gYbi-_hQc@#Cc-Z{{t59R zi<;ISXTT<~fbPYqYV>~sb-Tqo6yE4rg? zOpZ>d7cV^<8GTlQ&xoqmDb+6yYU*{moGym8aZt+zkC6w;$scMKIRBI7Zx~_k?%i`T zn>m|ip88q_by8Ug8m2k7OP7=#drJ48IRO6Q+%l`!Hkdm+8st%rm=t3W_il8xETe_e zDi+Plela4e`^hive>AJN!2M2uVX)~%esXp-y=F^=h{uR8hIx}uFmX%&yy~6Y8R@U+#q=VKIOu&iZ$u6*VjX;e->Nn~5I)g4;3yp`5y82!P6x+p9$t3@_DE zooRb{bqgB0b!(n~sRUE!{rNE(EbSdB`Lj>Zusza3;(b=Rnam3YJUKz{zM~sYyN#Wt z#p``SwM=Uy#xdC#)Of#&EKkzHkE=*A*a)wP(q4k@U zM2}*}KXZjFUk4iFKaMr*q!rPvA4h(3X-F-0jXE_o&Xt{)kfZUGQzv>Kq}kFg*SfFT zzA#k{*K7{yvdE55rzGav)>#_Fy;%QE%I_L8-BPLlcPd0z*I{6>!I_0=JWu}&f(qzv4HXM^vE&4jDNAMP6bOW)O zLH2XA^f!OvZH>*-t-D;EyG*GWv&3YzyE2km#g~uXJ}yZn0EHoAX+WQ~s>rGDsE~@3 z+Iz8bpwN!$E?c0WqEGYR%Lv?$ULOtgml8r#H4@-H%mE|a&t?sE#bS?+9XE9{7qcR& z9>`M{Z>%U$BPN7WSxxJsEU~|hs^)(d5KcD#p-#|oKIda^Iu{9J4+F>}=wXgg15vrp z7+{;AVp0WKVnQ4N-ZioacCs5)*KJy2e5WoEzRiB!Sxky=<9ihFmICCd@jVgOg z!>-5Akbo-x-9KcMLWIk|fio1qQAx{V$Q535SJCU(9m@U#EG=mMa-Xdj%A$MZSPa$u zGno>n!NR2LmE>t|IHOJu5&)6^!(8)-L8jXV<$G$$W2#zeaon*rUG5n`!&~^&g!vLdM98ThGWQ{yjGi_1ZBz-lX!4O zDXsf9$ATSxc+)KLenvW@xJXoNpwN@bxGR;~AnwNj$GL{$}uZyl&Lrf{Y`|q(P?UN73QLw#{R8NcJpd^%>hq zVBp;Lb~r!i85>*|_C;hS1{XCU!IvF@vu=SR4hJaJFwz2`jxOp`%Wz!Zb3crP!+p{r`2{*yWp%G&=Y37Ir*kl45FwQtsskB zPauN4R&KWQ-gMU!L|_--^xddRt%K4)@oZa2ZBUox%?>9u|I0g|j+Lj>YLIq4Y%7l6 z?GcKXRj%F)RA#W+0=wh|-Md|(Pt=AKg;#Z5z8Yu!j-V6}$wtuG;aEhUfPV~tQgfVW znKQEPjA8m7b41bJXk}I2CD>9}$t4|b+wl2eS8G;kIVgaye{dk>H@0nAJBt=gtt`S*v$GNgE`Ze1mkGz^RteC>b&S^^_q%7gilx; zhRLpv#qY;NN$w|4CE}aXqT*4siev;*;-veFT9R#c20Bf!zE;I3XwSAK%`mnu)!p(t zzJi_CDrlwp0x6VKG((3n#gq-0p3;{A;~_Scz?t?&-10M<41nib==~ZW<~3Nm5^QMG zNum$v=VE;xdsEufWROO61e)fc|J>=NH;iJY-2!G!^hnmlR3w6P32DQkf*9ujGbp?0 z!Xr$tX$#Y$#!UCMl1D@DKGp6m(=-qY)Sxl_xM83MK}-30!)7WE8KgT~U|Z^ti)S`F zI2V!TR>D1=`LGCcNTnuqaxqLO;t@GTN@g5Fbl3bQ^qQDuDIqoy10FHEhYB_6_C;WdTt=7AZ$PEiz4+DW9~|(E*F4={+RL(Vw1_V z+BlWL=#AndYUeDlBhmHA3sg9!@-)AacN6zsj2)Etfv|}aEd^?wSxRn!aT+Uui`n_L zQVvvFD)?^3s5cJ`>ZaDN*m^PbL2n#^d*0){p}UdZvp7mjhIvK{K$gbWGYn6yg}EDh zJIzhXQjw=)D)BlXkiO>Mp#^}07*cT2Z5`P2N4l^fQo}qY_%vg5w6JmH+oeGcp&fB& z{bthr>5N23!4O0!H-{ddH(~0utU%Ed{Sx$VF-c9L7AI}xY*OPk4jqHr^c&)^g-1gh z3>uI`ESJ6=EsFQUvq+njG;``Z3O3d(4IpnSCiIGAAC#*vW(Y!mmxxOhPfm`eZNm_4 zE_n&7LWw%sTFN(qpPD#ocoW606Unji?GDHsmL=YSTdoF}Z$CxNTf0P=L(-5_bd38m zWe^@JYMQlWA%;5+jO#$+!M23g`1xM3xF@Pl!Y3nl&fQj#2)#^5b+Zz|y-?1}CL4}Z zE(DfAi8QEdyjBs&Y+g|9>Y z#f2)@rV{*M7e@QzviL>qN4EY1D-!8&*70NLL;@Gwc2)uamm3VG-z|#=`I+T-?pjwu zU)Y$m{_Wm?En8A26`Qogq?G8ou*+z*gD2X;Co}L`=@BI56pJdgi_%h%maN!u&y=Gl z4L;~#*DLOmQ)zd5taLeUQt0c*Lq@8*#w zi)OL>o7C$2c$iqtXEziXSOJWHyV5Gz8!Wy~Lz^fWo!7DKq%(X?K>jL5n~7?u0d9&>v@t6D6D5tC#p06=~pDOs~p>$ zvX*CQE=<_NS{LF|`ADo2N zSy%*FP3_}GkJp=AImL2H(e4KvQ|0PrI^M_c@x&W!Ph}#1*14MvlTox3f1|y>jVJSs zyo>f=ySTS&VBDf1)eE%3Fdg1`9v)#*T&4o2sO&^ReA6!W3MeMwYiog&XuWBsQ(jux zJeBH>)u6V#J)liJJY9_QwnuR+0sNajcYs>Sne2E9Ysu^5lQ6@Q?O=z3!Zs;!LSJ`$ zc*g0gsRDb(v~=D;<_aWPS&_7Wtm{mL%cx|NTCD>uko+Jvb`X~iQ30Tp>koZd#j+7T zf)3h-#w314xD5H01BMghN; zb65b#$QrA;LyAa?RC^{3(D;}Kg3t+9sc)SgnCZI5(cI{D>XLR-To-0sbJleck_rB} z>8TBDLrI22vk-Ipc;sL5OXH~^E|Hte?hs}dLFXE#Ykw~d93!V9DV>l{1|sl^89ZN& z2@|@zSZb!@S{?v!2uN3hgz5M8euO(8vsI<_2>CiM!y&Kc)~c%(o5J5=)OQvomL|~g zJCKRr%s~z<&!<^cCM|J{f5M+0D+>bBf7CaDieIVmS*}gIZ;MA6A)EKFI3K=vY(ga> z&SynqA3NH^{*7`6o$vKhc!0%DYWepe4SlZGi=5HmH=lEnW6PgV55@emf%?cxd6OL{ zUD>Ty?l(%>0H`gwkCl8p<_hs>p(%j5;)=ot_4?trRqsl9#5vng5$S1OxBsZjtFK(6 z_IypvUfapf$ay5fnHxT8zm#FDja4+bztG#ypbwCx9hgq9Dhy27!)U?0N&36z*oOFc z4xyM^mqQPq_Rh`9yVTX23O9u><`Y*csJm%IFr*Pps*fe%q0vQZHfiIaR|=230Qtnv zpgCjj>Po^VtW_gDgf9Kpll}9cKYnZpT#5LV1|p61vGdc%q`iS=k(xX>-TGSIUKH z-aOVJs;S(nygh{qD&+^Emzh^n-QQVJvl43kE6O3)BU3i6#Sx<4{U3f{{4&K z(c?QVuKMs>6~s50o~6A`Z%BWK$H~$3Bn#lZ!NC5Cj?*hu#Gpm=d)esPeDmDI`>2@O zo8tP&VmaEvdfYJ85v2I~xjg(P+&jtx@l+rC7LEEW`N)NwXZ z^68Ix#yHR`{S2P7z|%H=ZcB6>HneO*+CL5SNrvzyl~mCN>wHnP20q-#KibbpW33QP z&1Fl9(33og0=CMgPMLF~wq|t$twamvR6;^>oKEunGI894dp?`+UsN)?k~b0{lA5Ht@(+ z!&BdzU3%EL&h1*S0BTDG%Y+`sc=FUw=DMOB1qw@I$ce=j34e)lj`OzLPKbqBp86Gk zlS=S&xJZ{xgc4f)&8DtD7z`e}&-~z6!d2I-bymo|LqN)m(rZi9YF@1*>?*_yY$jJOjb4A)nlabX0sv%N3 z#rkrck%|%`SU#yQnoXfGtGmsdN3u>jOf4?XE6#XV<5v1zyt=V}tntqeV~*J86PE>cGo;Uc*fXI>UqR7Ov%e3u4AH8z9uwGo)!I_@!mr@ zyslbGMnAgKU#zgE<=AJR=(~rHW{gx2h!B;qR^`XQxF3VWAueEEFgi;EuYsZ`P~|4- zmgzPBkV3j(hq4~B`0$}2d*E9j1!lt~8hWXZWzLh2$Na6qr}$)=(&S{YGu}+92FfV> zP(4m;cJ6NmMXweb4MIxq)u+3DASXEuS!NQ+jH+ZN9pNB-gapyHI%pdgVv5dL> zVWW#33W}xp>7+l#6H9k@#*min)2;GYmpgbPC?4`<-2OyK64RuwYR@BT%rAD74Jep- zXXVoFDf&o)wm^NQZS(3{r@}I=L`q_)nti)SV@Sy)FCq1bX zU=%Qdk5AUM`qZpXS+p$XLO#kdBBj{eJa(~F5UzD@6Zdq48k@KF3rC0AgYH$i*owc8IZPNw%PC8_+ArG6_)^|0s9NKIEY@OG2}ey5YH zb!+Y5a$m|=k*BKUVxHE|oP?6x61JK0<%Uoedg^6N%B2Y^Rx2?Vy;{*lf{}r3*kJFg zRu4cZ^H1a6L}{lK>`@XVY&32krV(QAJ#I=hP0qb+b-E|E^<=gb3IM6j)%_V&U5>={d2fjsPyNG(;kY#STa#(8g*$=%N zOe&t|a@p`m4^QWGpVIWVI3^{y6JT*nUudALMV;$3DkDFkjPu^MNf&VmTs-dCD#1op z29bmzIt^vb1AC5B)w3CeMahv$YHfdGF{d6_BJMxpp5OM5sR(SGLk%R~!g}zg(25=v z2dvd?c{JXW*lGtz^LE=42!Z8{YaUyZav0b>ok;o!m4#P?Gn8P>voE-bt)*vQ4{G|c zMvbyV?{PbdxHPBa?U{2{_KgiO5r9t}HtDC@qDHfXu`lylc|8nnIIu`=i_3idN)%?x z2pka?IEqu}78d<%)7YAOQ+q(WouH>cIu;-(Yri4_^H87J13!kh@kv)r(D9{E18s5i7|wOF>=LrxHa zvNK6$5%zKjH&Ale!`Z6w5DL|KC$CAqFNsW@*IO?8!%ny?KGL}VbZAGweIts7;LI)r z%V~ew(Ywn;z@yO_ms3EJvPrnCSE|w)SN!V|afg$jd1@>tUrku_zkV7+chMY#1Oxxkk}C;(q`b_6Fww From 1d462bbe3713bc2fea40ed45c80a06ce856d379f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=94=B0=E6=AC=A7?= Date: Thu, 21 Mar 2019 15:12:06 +0800 Subject: [PATCH 02/10] chore: update ginS (#1822) --- ginS/gins.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/ginS/gins.go b/ginS/gins.go index 0f08645a..3ce4a6f6 100644 --- a/ginS/gins.go +++ b/ginS/gins.go @@ -125,23 +125,35 @@ func Use(middlewares ...gin.HandlerFunc) gin.IRoutes { return engine().Use(middlewares...) } -// Run : The router is attached to a http.Server and starts listening and serving HTTP requests. +// Routes returns a slice of registered routes. +func Routes() gin.RoutesInfo { + return engine().Routes() +} + +// Run attaches 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 indefinitely unless an error happens. func Run(addr ...string) (err error) { return engine().Run(addr...) } -// RunTLS : The router is attached to a http.Server and starts listening and serving HTTPS requests. +// RunTLS attaches 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 indefinitely unless an error happens. func RunTLS(addr, certFile, keyFile string) (err error) { return engine().RunTLS(addr, certFile, keyFile) } -// RunUnix : The router is attached to a http.Server and starts listening and serving HTTP requests +// RunUnix attaches 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 indefinitely unless an error happens. func RunUnix(file string) (err error) { return engine().RunUnix(file) } + +// RunFd attaches the router to a http.Server and starts listening and serving HTTP requests +// through the specified file descriptor. +// Note: thie method will block the calling goroutine indefinitely unless on error happens. +func RunFd(fd int) (err error) { + return engine().RunFd(fd) +} From ce20f107f5dc498ec7489d7739541a25dcd48463 Mon Sep 17 00:00:00 2001 From: Dan Markham Date: Wed, 27 Mar 2019 23:14:00 -0700 Subject: [PATCH 03/10] Truncate Latency precision in long running request (#1830) fixes #1823 --- logger.go | 4 ++++ logger_test.go | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/logger.go b/logger.go index 198a0192..5ab4639e 100644 --- a/logger.go +++ b/logger.go @@ -136,6 +136,10 @@ var defaultLogFormatter = func(param LogFormatterParams) string { resetColor = param.ResetColor() } + if param.Latency > time.Minute { + // Truncate in a golang < 1.8 safe way + param.Latency = param.Latency - param.Latency%time.Second + } return fmt.Sprintf("[GIN] %v |%s %3d %s| %13v | %15s |%s %-7s %s %s\n%s", param.TimeStamp.Format("2006/01/02 - 15:04:05"), statusColor, param.StatusCode, resetColor, diff --git a/logger_test.go b/logger_test.go index 11a859e6..56bb3a00 100644 --- a/logger_test.go +++ b/logger_test.go @@ -253,10 +253,34 @@ func TestDefaultLogFormatter(t *testing.T) { ErrorMessage: "", isTerm: true, } + termTrueLongDurationParam := LogFormatterParams{ + TimeStamp: timeStamp, + StatusCode: 200, + Latency: time.Millisecond * 9876543210, + ClientIP: "20.20.20.20", + Method: "GET", + Path: "/", + ErrorMessage: "", + isTerm: true, + } + + termFalseLongDurationParam := LogFormatterParams{ + TimeStamp: timeStamp, + StatusCode: 200, + Latency: time.Millisecond * 9876543210, + ClientIP: "20.20.20.20", + Method: "GET", + Path: "/", + ErrorMessage: "", + isTerm: false, + } assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 5s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseParam)) + assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 | 200 | 2743h29m3s | 20.20.20.20 | GET /\n", defaultLogFormatter(termFalseLongDurationParam)) assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 5s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueParam)) + assert.Equal(t, "[GIN] 2018/12/07 - 09:11:42 |\x1b[97;42m 200 \x1b[0m| 2743h29m3s | 20.20.20.20 |\x1b[97;44m GET \x1b[0m /\n", defaultLogFormatter(termTrueLongDurationParam)) + } func TestColorForMethod(t *testing.T) { From 2e915f4e5083995154f65a600c86582b5396d02a Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Tue, 2 Apr 2019 04:01:34 +0300 Subject: [PATCH 04/10] refactor(form_mapping.go): mapping multipart request (#1829) * refactor(form_mapping.go): mapping multipart request * add checkers for a types to match with the setter interface * form_mapping.go: rename method name on setter interface, add comments * fix style of comments --- binding/form.go | 36 +++++++++++++++--- binding/form_mapping.go | 84 ++++++++++++++++++++--------------------- 2 files changed, 71 insertions(+), 49 deletions(-) diff --git a/binding/form.go b/binding/form.go index f1f89195..0b28aa8a 100644 --- a/binding/form.go +++ b/binding/form.go @@ -4,7 +4,11 @@ package binding -import "net/http" +import ( + "mime/multipart" + "net/http" + "reflect" +) const defaultMemory = 32 * 1024 * 1024 @@ -53,13 +57,33 @@ func (formMultipartBinding) Bind(req *http.Request, obj interface{}) error { if err := req.ParseMultipartForm(defaultMemory); err != nil { return err } - if err := mapForm(obj, req.MultipartForm.Value); err != nil { - return err - } - - if err := mapFiles(obj, req); err != nil { + if err := mappingByPtr(obj, (*multipartRequest)(req), "form"); err != nil { return err } return validate(obj) } + +type multipartRequest http.Request + +var _ setter = (*multipartRequest)(nil) + +var ( + multipartFileHeaderStructType = reflect.TypeOf(multipart.FileHeader{}) +) + +// TrySet tries to set a value by the multipart request with the binding a form file +func (r *multipartRequest) TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) { + if value.Type() == multipartFileHeaderStructType { + _, file, err := (*http.Request)(r).FormFile(key) + if err != nil { + return false, err + } + if file != nil { + value.Set(reflect.ValueOf(*file)) + return true, nil + } + } + + return setByForm(value, field, r.MultipartForm.Value, key, opt) +} diff --git a/binding/form_mapping.go b/binding/form_mapping.go index fc33b1df..aaacf6c5 100644 --- a/binding/form_mapping.go +++ b/binding/form_mapping.go @@ -7,7 +7,6 @@ package binding import ( "errors" "fmt" - "net/http" "reflect" "strconv" "strings" @@ -16,34 +15,6 @@ import ( "github.com/gin-gonic/gin/internal/json" ) -func mapFiles(ptr interface{}, req *http.Request) 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) - - t := fmt.Sprintf("%s", typeField.Type) - if string(t) != "*multipart.FileHeader" { - continue - } - - inputFieldName := typeField.Tag.Get("form") - if inputFieldName == "" { - inputFieldName = typeField.Name - } - - _, fileHeader, err := req.FormFile(inputFieldName) - if err != nil { - return err - } - - structField.Set(reflect.ValueOf(fileHeader)) - - } - return nil -} - var errUnknownType = errors.New("Unknown type") func mapUri(ptr interface{}, m map[string][]string) error { @@ -57,11 +28,29 @@ func mapForm(ptr interface{}, form map[string][]string) error { var emptyField = reflect.StructField{} func mapFormByTag(ptr interface{}, form map[string][]string, tag string) error { - _, err := mapping(reflect.ValueOf(ptr), emptyField, form, tag) + return mappingByPtr(ptr, formSource(form), tag) +} + +// setter tries to set value on a walking by fields of a struct +type setter interface { + TrySet(value reflect.Value, field reflect.StructField, key string, opt setOptions) (isSetted bool, err error) +} + +type formSource map[string][]string + +var _ setter = formSource(nil) + +// TrySet tries to set a value by request's form source (like map[string][]string) +func (form formSource) TrySet(value reflect.Value, field reflect.StructField, tagValue string, opt setOptions) (isSetted bool, err error) { + return setByForm(value, field, form, tagValue, opt) +} + +func mappingByPtr(ptr interface{}, setter setter, tag string) error { + _, err := mapping(reflect.ValueOf(ptr), emptyField, setter, tag) return err } -func mapping(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) { +func mapping(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { var vKind = value.Kind() if vKind == reflect.Ptr { @@ -71,7 +60,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s isNew = true vPtr = reflect.New(value.Type().Elem()) } - isSetted, err := mapping(vPtr.Elem(), field, form, tag) + isSetted, err := mapping(vPtr.Elem(), field, setter, tag) if err != nil { return false, err } @@ -81,7 +70,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s return isSetted, nil } - ok, err := tryToSetValue(value, field, form, tag) + ok, err := tryToSetValue(value, field, setter, tag) if err != nil { return false, err } @@ -97,7 +86,7 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s if !value.Field(i).CanSet() { continue } - ok, err := mapping(value.Field(i), tValue.Field(i), form, tag) + ok, err := mapping(value.Field(i), tValue.Field(i), setter, tag) if err != nil { return false, err } @@ -108,9 +97,14 @@ func mapping(value reflect.Value, field reflect.StructField, form map[string][]s return false, nil } -func tryToSetValue(value reflect.Value, field reflect.StructField, form map[string][]string, tag string) (bool, error) { - var tagValue, defaultValue string - var isDefaultExists bool +type setOptions struct { + isDefaultExists bool + defaultValue string +} + +func tryToSetValue(value reflect.Value, field reflect.StructField, setter setter, tag string) (bool, error) { + var tagValue string + var setOpt setOptions tagValue = field.Tag.Get(tag) tagValue, opts := head(tagValue, ",") @@ -132,25 +126,29 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri k, v := head(opt, "=") switch k { case "default": - isDefaultExists = true - defaultValue = v + setOpt.isDefaultExists = true + setOpt.defaultValue = v } } + return setter.TrySet(value, field, tagValue, setOpt) +} + +func setByForm(value reflect.Value, field reflect.StructField, form map[string][]string, tagValue string, opt setOptions) (isSetted bool, err error) { vs, ok := form[tagValue] - if !ok && !isDefaultExists { + if !ok && !opt.isDefaultExists { return false, nil } switch value.Kind() { case reflect.Slice: if !ok { - vs = []string{defaultValue} + vs = []string{opt.defaultValue} } return true, setSlice(vs, value, field) case reflect.Array: if !ok { - vs = []string{defaultValue} + vs = []string{opt.defaultValue} } if len(vs) != value.Len() { return false, fmt.Errorf("%q is not valid value for %s", vs, value.Type().String()) @@ -159,7 +157,7 @@ func tryToSetValue(value reflect.Value, field reflect.StructField, form map[stri default: var val string if !ok { - val = defaultValue + val = opt.defaultValue } if len(vs) > 0 { From ffcbe77b1e6222b4e0e97eb1920adc1813fb2224 Mon Sep 17 00:00:00 2001 From: Eason Lin Date: Sat, 6 Apr 2019 21:48:33 +0800 Subject: [PATCH 05/10] chore(readme): rollback readme (#1846) #1844 #1838 Keep the documentation in readme until full available on the new website. --- README.md | 2010 ++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 1972 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index d3433ed2..804041f9 100644 --- a/README.md +++ b/README.md @@ -13,35 +13,122 @@ Gin is a web framework written in Go (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. -**The key features of Gin are:** -- Zero allocation router -- Fast -- Middleware support -- Crash-free -- JSON validation -- Routes grouping -- Error management -- Rendering built-in -- Extendable +## Contents -For more feature details, please see the [Gin website introduction](https://gin-gonic.com/docs/introduction/). +- [Installation](#installation) +- [Prerequisite](#prerequisite) +- [Quick start](#quick-start) +- [Benchmarks](#benchmarks) +- [Gin v1.stable](#gin-v1-stable) +- [Build with jsoniter](#build-with-jsoniter) +- [API Examples](#api-examples) + - [Using GET,POST,PUT,PATCH,DELETE and OPTIONS](#using-get-post-put-patch-delete-and-options) + - [Parameters in path](#parameters-in-path) + - [Querystring parameters](#querystring-parameters) + - [Multipart/Urlencoded Form](#multiparturlencoded-form) + - [Another example: query + post form](#another-example-query--post-form) + - [Map as querystring or postform parameters](#map-as-querystring-or-postform-parameters) + - [Upload files](#upload-files) + - [Grouping routes](#grouping-routes) + - [Blank Gin without middleware by default](#blank-gin-without-middleware-by-default) + - [Using middleware](#using-middleware) + - [How to write log file](#how-to-write-log-file) + - [Custom Log Format](#custom-log-format) + - [Model binding and validation](#model-binding-and-validation) + - [Custom Validators](#custom-validators) + - [Only Bind Query String](#only-bind-query-string) + - [Bind Query String or Post Data](#bind-query-string-or-post-data) + - [Bind Uri](#bind-uri) + - [Bind HTML checkboxes](#bind-html-checkboxes) + - [Multipart/Urlencoded binding](#multiparturlencoded-binding) + - [XML, JSON, YAML and ProtoBuf rendering](#xml-json-yaml-and-protobuf-rendering) + - [JSONP rendering](#jsonp) + - [Serving static files](#serving-static-files) + - [Serving data from reader](#serving-data-from-reader) + - [HTML rendering](#html-rendering) + - [Multitemplate](#multitemplate) + - [Redirects](#redirects) + - [Custom Middleware](#custom-middleware) + - [Using BasicAuth() middleware](#using-basicauth-middleware) + - [Goroutines inside a middleware](#goroutines-inside-a-middleware) + - [Custom HTTP configuration](#custom-http-configuration) + - [Support Let's Encrypt](#support-lets-encrypt) + - [Run multiple service using Gin](#run-multiple-service-using-gin) + - [Graceful restart or stop](#graceful-restart-or-stop) + - [Build a single binary with templates](#build-a-single-binary-with-templates) + - [Bind form-data request with custom struct](#bind-form-data-request-with-custom-struct) + - [Try to bind body into different structs](#try-to-bind-body-into-different-structs) + - [http2 server push](#http2-server-push) + - [Define format for the log of routes](#define-format-for-the-log-of-routes) + - [Set and get a cookie](#set-and-get-a-cookie) +- [Testing](#testing) +- [Users](#users) -## Getting started +## Installation -### Getting Gin +To install Gin package, you need to install Go and set your Go workspace first. -The first need [Go](https://golang.org/) installed (**version 1.6+ is required**), then you can use the below Go command to install Gin. +1. Download and install it: ```sh $ go get -u github.com/gin-gonic/gin ``` -For more installation guides such as vendor tool, please check out [Gin quickstart](https://gin-gonic.com/docs/quickstart/). +2. Import it in your code: -### Running Gin +```go +import "github.com/gin-gonic/gin" +``` -First you need to import Gin package for using Gin, one simplest example likes the follow `example.go`: +3. (Optional) Import `net/http`. This is required for example if using constants such as `http.StatusOK`. + +```go +import "net/http" +``` + +### Use a vendor tool like [Govendor](https://github.com/kardianos/govendor) + +1. `go get` govendor + +```sh +$ go get github.com/kardianos/govendor +``` +2. Create your project folder and `cd` inside + +```sh +$ mkdir -p $GOPATH/src/github.com/myusername/project && cd "$_" +``` + +3. Vendor init your project and add gin + +```sh +$ govendor init +$ govendor fetch github.com/gin-gonic/gin@v1.3 +``` + +4. Copy a starting template inside your project + +```sh +$ curl https://raw.githubusercontent.com/gin-gonic/examples/master/basic/main.go > main.go +``` + +5. Run your project + +```sh +$ go run main.go +``` + +## Prerequisite + +Now Gin requires Go 1.6 or later and Go 1.7 will be required soon. + +## Quick start + +```sh +# assume the following codes in example.go file +$ cat example.go +``` ```go package main @@ -59,8 +146,6 @@ func main() { } ``` -And use the Go command to run the demo: - ``` # run example.go and visit 0.0.0.0:8080/ping on browser $ go run example.go @@ -68,7 +153,9 @@ $ go run example.go ## Benchmarks -Please see all benchmarks details from [Gin website](https://gin-gonic.com/docs/benchmarks/). +Gin uses a custom version of [HttpRouter](https://github.com/julienschmidt/httprouter) + +[See all benchmarks](/BENCHMARKS.md) Benchmark name | (1) | (2) | (3) | (4) --------------------------------------------|-----------:|------------:|-----------:|---------: @@ -105,32 +192,1879 @@ BenchmarkVulcan_GithubAll | 5000 | 394253 | 19894 - (3): Heap Memory (B/op), lower is better - (4): Average Allocations per Repetition (allocs/op), lower is better -## Middlewares +## Gin v1. stable -You can find many useful Gin middlewares at [gin-contrib](https://github.com/gin-contrib). +- [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. -## Documentation +## Build with [jsoniter](https://github.com/json-iterator/go) -See [API documentation and descriptions](https://godoc.org/github.com/gin-gonic/gin) for package. +Gin uses `encoding/json` as default json package but you can change to [jsoniter](https://github.com/json-iterator/go) by build from other tags. -All documentation is available on the Gin website. +```sh +$ go build -tags=jsoniter . +``` -- [English](https://gin-gonic.com/docs/) -- [简体中文](https://gin-gonic.com/zh-cn/docs/) -- [繁體中文](https://gin-gonic.com/zh-tw/docs/) -- [日本語](https://gin-gonic.com/ja/docs/) +## API Examples -## Examples +You can find a number of ready-to-run examples at [Gin examples repository](https://github.com/gin-gonic/examples). -A number of ready-to-run examples demonstrating various use cases of Gin on the [Gin examples](https://github.com/gin-gonic/examples) repository. +### Using GET, POST, PUT, PATCH, DELETE and OPTIONS + +```go +func main() { + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + 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) + + // By default it serves on :8080 unless a + // PORT environment variable was defined. + router.Run() + // router.Run(":3000") for a hard coded port +} +``` + +### Parameters in path + +```go +func main() { + router := gin.Default() + + // This handler will match /user/john but will not match /user/ or /user + 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 + // If no other routers match /user/john, it will redirect to /user/john/ + router.GET("/user/:name/*action", func(c *gin.Context) { + name := c.Param("name") + action := c.Param("action") + message := name + " is " + action + c.String(http.StatusOK, message) + }) + + 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, + "nick": nick, + }) + }) + router.Run(":8080") +} +``` + +### Another example: query + post form + +``` +POST /post?id=1234&page=1 HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + +name=manu&message=this_is_great +``` + +```go +func main() { + router := gin.Default() + + router.POST("/post", func(c *gin.Context) { + + id := c.Query("id") + page := c.DefaultQuery("page", "0") + name := c.PostForm("name") + message := c.PostForm("message") + + fmt.Printf("id: %s; page: %s; name: %s; message: %s", id, page, name, message) + }) + router.Run(":8080") +} +``` + +``` +id: 1234; page: 1; name: manu; message: this_is_great +``` + +### Map as querystring or postform parameters + +``` +POST /post?ids[a]=1234&ids[b]=hello HTTP/1.1 +Content-Type: application/x-www-form-urlencoded + +names[first]=thinkerou&names[second]=tianou +``` + +```go +func main() { + router := gin.Default() + + router.POST("/post", func(c *gin.Context) { + + ids := c.QueryMap("ids") + names := c.PostFormMap("names") + + fmt.Printf("ids: %v; names: %v", ids, names) + }) + router.Run(":8080") +} +``` + +``` +ids: map[b:hello a:1234], names: map[second:tianou first:thinkerou] +``` + +### Upload files + +#### Single file + +References issue [#774](https://github.com/gin-gonic/gin/issues/774) and detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/single). + +`file.Filename` **SHOULD NOT** be trusted. See [`Content-Disposition` on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#Directives) and [#1693](https://github.com/gin-gonic/gin/issues/1693) + +> The filename is always optional and must not be used blindly by the application: path information should be stripped, and conversion to the server file system rules should be done. + +```go +func main() { + router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + // router.MaxMultipartMemory = 8 << 20 // 8 MiB + router.POST("/upload", func(c *gin.Context) { + // single file + file, _ := c.FormFile("file") + log.Println(file.Filename) + + // Upload the file to specific dst. + // c.SaveUploadedFile(file, dst) + + c.String(http.StatusOK, fmt.Sprintf("'%s' uploaded!", file.Filename)) + }) + router.Run(":8080") +} +``` + +How to `curl`: + +```bash +curl -X POST http://localhost:8080/upload \ + -F "file=@/Users/appleboy/test.zip" \ + -H "Content-Type: multipart/form-data" +``` + +#### Multiple files + +See the detail [example code](https://github.com/gin-gonic/examples/tree/master/upload-file/multiple). + +```go +func main() { + router := gin.Default() + // Set a lower memory limit for multipart forms (default is 32 MiB) + // router.MaxMultipartMemory = 8 << 20 // 8 MiB + router.POST("/upload", func(c *gin.Context) { + // Multipart form + form, _ := c.MultipartForm() + files := form.File["upload[]"] + + for _, file := range files { + log.Println(file.Filename) + + // Upload the file to specific dst. + // c.SaveUploadedFile(file, dst) + } + c.String(http.StatusOK, fmt.Sprintf("%d files uploaded!", len(files))) + }) + router.Run(":8080") +} +``` + +How to `curl`: + +```bash +curl -X POST http://localhost:8080/upload \ + -F "upload[]=@/Users/appleboy/test1.zip" \ + -F "upload[]=@/Users/appleboy/test2.zip" \ + -H "Content-Type: multipart/form-data" +``` + +### Grouping routes + +```go +func main() { + router := gin.Default() + + // Simple group: v1 + v1 := router.Group("/v1") + { + v1.POST("/login", loginEndpoint) + v1.POST("/submit", submitEndpoint) + v1.POST("/read", readEndpoint) + } + + // Simple group: v2 + v2 := router.Group("/v2") + { + v2.POST("/login", loginEndpoint) + v2.POST("/submit", submitEndpoint) + v2.POST("/read", readEndpoint) + } + + router.Run(":8080") +} +``` + +### Blank Gin without middleware by default + +Use + +```go +r := gin.New() +``` + +instead of + +```go +// Default With the Logger and Recovery middleware already attached +r := gin.Default() +``` + + +### Using middleware +```go +func main() { + // Creates a router without any middleware by default + r := gin.New() + + // Global middleware + // Logger middleware will write the logs to gin.DefaultWriter even if you set with GIN_MODE=release. + // By default gin.DefaultWriter = os.Stdout + r.Use(gin.Logger()) + + // Recovery middleware recovers from any panics and writes a 500 if there was one. + r.Use(gin.Recovery()) + + // 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 as: + authorized := r.Group("/") + // per group middleware! in this case we use the custom created + // AuthRequired() middleware just in the "authorized" group. + authorized.Use(AuthRequired()) + { + authorized.POST("/login", loginEndpoint) + authorized.POST("/submit", submitEndpoint) + authorized.POST("/read", readEndpoint) + + // nested group + testing := authorized.Group("testing") + testing.GET("/analytics", analyticsEndpoint) + } + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### How to write log file +```go +func main() { + // Disable Console Color, you don't need console color when writing the logs to file. + gin.DisableConsoleColor() + + // Logging to a file. + f, _ := os.Create("gin.log") + gin.DefaultWriter = io.MultiWriter(f) + + // Use the following code if you need to write the logs to file and console at the same time. + // gin.DefaultWriter = io.MultiWriter(f, os.Stdout) + + router := gin.Default() + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + +    router.Run(":8080") +} +``` + +### Custom Log Format +```go +func main() { + router := gin.New() + + // LoggerWithFormatter middleware will write the logs to gin.DefaultWriter + // By default gin.DefaultWriter = os.Stdout + router.Use(gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string { + + // your custom format + return fmt.Sprintf("%s - [%s] \"%s %s %s %d %s \"%s\" %s\"\n", + param.ClientIP, + param.TimeStamp.Format(time.RFC1123), + param.Method, + param.Path, + param.Request.Proto, + param.StatusCode, + param.Latency, + param.Request.UserAgent(), + param.ErrorMessage, + ) + })) + router.Use(gin.Recovery()) + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +**Sample Output** +``` +::1 - [Fri, 07 Dec 2018 17:04:38 JST] "GET /ping HTTP/1.1 200 122.767µs "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.80 Safari/537.36" " +``` + +### Controlling Log output coloring + +By default, logs output on console should be colorized depending on the detected TTY. + +Never colorize logs: + +```go +func main() { + // Disable log's color + gin.DisableConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +Always colorize logs: + +```go +func main() { + // Force log's color + gin.ForceConsoleColor() + + // Creates a gin router with default middleware: + // logger and recovery (crash-free) middleware + router := gin.Default() + + router.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + router.Run(":8080") +} +``` + +### Model binding and validation + +To bind a request body into a type, use model binding. We currently support binding of JSON, XML, YAML and standard form values (foo=bar&boo=baz). + +Gin uses [**go-playground/validator.v8**](https://github.com/go-playground/validator) for validation. Check the full docs on tags usage [here](http://godoc.org/gopkg.in/go-playground/validator.v8#hdr-Baked_In_Validators_and_Tags). + +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"`. + +Also, Gin provides two sets of methods for binding: +- **Type** - Must bind + - **Methods** - `Bind`, `BindJSON`, `BindXML`, `BindQuery`, `BindYAML` + - **Behavior** - These methods use `MustBindWith` under the hood. If there is a binding error, the request is aborted with `c.AbortWithError(400, err).SetType(ErrorTypeBind)`. This sets the response status code to 400 and the `Content-Type` header is set to `text/plain; charset=utf-8`. Note that if you try to set the response code after this, it will result in a warning `[GIN-debug] [WARNING] Headers were already written. Wanted to override status code 400 with 422`. If you wish to have greater control over the behavior, consider using the `ShouldBind` equivalent method. +- **Type** - Should bind + - **Methods** - `ShouldBind`, `ShouldBindJSON`, `ShouldBindXML`, `ShouldBindQuery`, `ShouldBindYAML` + - **Behavior** - These methods use `ShouldBindWith` under the hood. If there is a binding error, the error is returned and it is the developer's responsibility to handle the request and error appropriately. + +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 `MustBindWith` or `ShouldBindWith`. + +You can also specify that specific fields are required. If a field is decorated with `binding:"required"` and has a empty value when binding, an error will be returned. + +```go +// Binding from JSON +type Login struct { + User string `form:"user" json:"user" xml:"user" binding:"required"` + Password string `form:"password" json:"password" xml:"password" binding:"required"` +} + +func main() { + router := gin.Default() + + // Example for binding JSON ({"user": "manu", "password": "123"}) + router.POST("/loginJSON", func(c *gin.Context) { + var json Login + if err := c.ShouldBindJSON(&json); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if json.User != "manu" || json.Password != "123" { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + }) + + // Example for binding XML ( + // + // + // user + // 123 + // ) + router.POST("/loginXML", func(c *gin.Context) { + var xml Login + if err := c.ShouldBindXML(&xml); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if xml.User != "manu" || xml.Password != "123" { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + }) + + // Example for binding a HTML form (user=manu&password=123) + router.POST("/loginForm", func(c *gin.Context) { + var form Login + // This will infer what binder to use depending on the content-type header. + if err := c.ShouldBind(&form); err != nil { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + if form.User != "manu" || form.Password != "123" { + c.JSON(http.StatusUnauthorized, gin.H{"status": "unauthorized"}) + return + } + + c.JSON(http.StatusOK, gin.H{"status": "you are logged in"}) + }) + + // Listen and serve on 0.0.0.0:8080 + router.Run(":8080") +} +``` + +**Sample request** +```shell +$ curl -v -X POST \ + http://localhost:8080/loginJSON \ + -H 'content-type: application/json' \ + -d '{ "user": "manu" }' +> POST /loginJSON HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.51.0 +> Accept: */* +> content-type: application/json +> Content-Length: 18 +> +* upload completely sent off: 18 out of 18 bytes +< HTTP/1.1 400 Bad Request +< Content-Type: application/json; charset=utf-8 +< Date: Fri, 04 Aug 2017 03:51:31 GMT +< Content-Length: 100 +< +{"error":"Key: 'Login.Password' Error:Field validation for 'Password' failed on the 'required' tag"} +``` + +**Skip validate** + +When running the above example using the above the `curl` command, it returns error. Because the example use `binding:"required"` for `Password`. If use `binding:"-"` for `Password`, then it will not return error when running the above example again. + +### Custom Validators + +It is also possible to register custom validators. See the [example code](https://github.com/gin-gonic/examples/tree/master/custom-validation/server.go). + +```go +package main + +import ( + "net/http" + "reflect" + "time" + + "github.com/gin-gonic/gin" + "github.com/gin-gonic/gin/binding" + "gopkg.in/go-playground/validator.v8" +) + +// Booking contains binded and validated data. +type Booking struct { + CheckIn time.Time `form:"check_in" binding:"required,bookabledate" time_format:"2006-01-02"` + CheckOut time.Time `form:"check_out" binding:"required,gtfield=CheckIn" time_format:"2006-01-02"` +} + +func bookableDate( + v *validator.Validate, topStruct reflect.Value, currentStructOrField reflect.Value, + field reflect.Value, fieldType reflect.Type, fieldKind reflect.Kind, param string, +) bool { + if date, ok := field.Interface().(time.Time); ok { + today := time.Now() + if today.Year() > date.Year() || today.YearDay() > date.YearDay() { + return false + } + } + return true +} + +func main() { + route := gin.Default() + + if v, ok := binding.Validator.Engine().(*validator.Validate); ok { + v.RegisterValidation("bookabledate", bookableDate) + } + + route.GET("/bookable", getBookable) + route.Run(":8085") +} + +func getBookable(c *gin.Context) { + var b Booking + if err := c.ShouldBindWith(&b, binding.Query); err == nil { + c.JSON(http.StatusOK, gin.H{"message": "Booking dates are valid!"}) + } else { + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + } +} +``` + +```console +$ curl "localhost:8085/bookable?check_in=2018-04-16&check_out=2018-04-17" +{"message":"Booking dates are valid!"} + +$ curl "localhost:8085/bookable?check_in=2018-03-08&check_out=2018-03-09" +{"error":"Key: 'Booking.CheckIn' Error:Field validation for 'CheckIn' failed on the 'bookabledate' tag"} +``` + +[Struct level validations](https://github.com/go-playground/validator/releases/tag/v8.7) can also be registered this way. +See the [struct-lvl-validation example](https://github.com/gin-gonic/examples/tree/master/struct-lvl-validations) to learn more. + +### Only Bind Query String + +`ShouldBindQuery` function only binds the query params and not the post data. See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-315953017). + +```go +package main + +import ( + "log" + + "github.com/gin-gonic/gin" +) + +type Person struct { + Name string `form:"name"` + Address string `form:"address"` +} + +func main() { + route := gin.Default() + route.Any("/testing", startPage) + route.Run(":8085") +} + +func startPage(c *gin.Context) { + var person Person + if c.ShouldBindQuery(&person) == nil { + log.Println("====== Only Bind By Query String ======") + log.Println(person.Name) + log.Println(person.Address) + } + c.String(200, "Success") +} + +``` + +### Bind Query String or Post Data + +See the [detail information](https://github.com/gin-gonic/gin/issues/742#issuecomment-264681292). + +```go +package main + +import ( + "log" + "time" + + "github.com/gin-gonic/gin" +) + +type Person struct { + Name string `form:"name"` + Address string `form:"address"` + Birthday time.Time `form:"birthday" time_format:"2006-01-02" time_utc:"1"` +} + +func main() { + route := gin.Default() + route.GET("/testing", startPage) + route.Run(":8085") +} + +func startPage(c *gin.Context) { + var person Person + // If `GET`, only `Form` binding engine (`query`) used. + // If `POST`, first checks the `content-type` for `JSON` or `XML`, then uses `Form` (`form-data`). + // See more at https://github.com/gin-gonic/gin/blob/master/binding/binding.go#L48 + if c.ShouldBind(&person) == nil { + log.Println(person.Name) + log.Println(person.Address) + log.Println(person.Birthday) + } + + c.String(200, "Success") +} +``` + +Test it with: +```sh +$ curl -X GET "localhost:8085/testing?name=appleboy&address=xyz&birthday=1992-03-15" +``` + +### Bind Uri + +See the [detail information](https://github.com/gin-gonic/gin/issues/846). + +```go +package main + +import "github.com/gin-gonic/gin" + +type Person struct { + ID string `uri:"id" binding:"required,uuid"` + Name string `uri:"name" binding:"required"` +} + +func main() { + route := gin.Default() + route.GET("/:name/:id", func(c *gin.Context) { + var person Person + if err := c.ShouldBindUri(&person); err != nil { + c.JSON(400, gin.H{"msg": err}) + return + } + c.JSON(200, gin.H{"name": person.Name, "uuid": person.ID}) + }) + route.Run(":8088") +} +``` + +Test it with: +```sh +$ curl -v localhost:8088/thinkerou/987fbc97-4bed-5078-9f07-9141ba07c9f3 +$ curl -v localhost:8088/thinkerou/not-uuid +``` + +### Bind HTML checkboxes + +See the [detail information](https://github.com/gin-gonic/gin/issues/129#issuecomment-124260092) + +main.go + +```go +... + +type myForm struct { + Colors []string `form:"colors[]"` +} + +... + +func formHandler(c *gin.Context) { + var fakeForm myForm + c.ShouldBind(&fakeForm) + c.JSON(200, gin.H{"color": fakeForm.Colors}) +} + +... + +``` + +form.html + +```html +
+

Check some colors

+ + + + + + + +
+``` + +result: + +``` +{"color":["red","green","blue"]} +``` + +### Multipart/Urlencoded binding + +```go +package main + +import ( + "github.com/gin-gonic/gin" +) + +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.ShouldBindWith(&form, binding.Form) + // or you can simply use autobinding with ShouldBind method: + var form LoginForm + // in this case proper binding will be automatically selected + if c.ShouldBind(&form) == nil { + 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: +```sh +$ curl -v --form user=user --form password=password http://localhost:8080/login +``` + +### XML, JSON, YAML and ProtoBuf rendering + +```go +func main() { + r := gin.Default() + + // gin.H is a shortcut for map[string]interface{} + r.GET("/someJSON", func(c *gin.Context) { + c.JSON(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) + }) + + r.GET("/moreJSON", func(c *gin.Context) { + // You also can use a struct + var msg struct { + Name string `json:"user"` + Message string + Number int + } + msg.Name = "Lena" + msg.Message = "hey" + msg.Number = 123 + // Note that msg.Name becomes "user" in the JSON + // Will output : {"user": "Lena", "Message": "hey", "Number": 123} + c.JSON(http.StatusOK, msg) + }) + + r.GET("/someXML", func(c *gin.Context) { + c.XML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) + }) + + r.GET("/someYAML", func(c *gin.Context) { + c.YAML(http.StatusOK, gin.H{"message": "hey", "status": http.StatusOK}) + }) + + r.GET("/someProtoBuf", func(c *gin.Context) { + reps := []int64{int64(1), int64(2)} + label := "test" + // The specific definition of protobuf is written in the testdata/protoexample file. + data := &protoexample.Test{ + Label: &label, + Reps: reps, + } + // Note that data becomes binary data in the response + // Will output protoexample.Test protobuf serialized data + c.ProtoBuf(http.StatusOK, data) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +#### SecureJSON + +Using SecureJSON to prevent json hijacking. Default prepends `"while(1),"` to response body if the given struct is array values. + +```go +func main() { + r := gin.Default() + + // You can also use your own secure json prefix + // r.SecureJsonPrefix(")]}',\n") + + r.GET("/someJSON", func(c *gin.Context) { + names := []string{"lena", "austin", "foo"} + + // Will output : while(1);["lena","austin","foo"] + c.SecureJSON(http.StatusOK, names) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` +#### JSONP + +Using JSONP to request data from a server in a different domain. Add callback to response body if the query parameter callback exists. + +```go +func main() { + r := gin.Default() + + r.GET("/JSONP?callback=x", func(c *gin.Context) { + data := map[string]interface{}{ + "foo": "bar", + } + + //callback is x + // Will output : x({\"foo\":\"bar\"}) + c.JSONP(http.StatusOK, data) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +#### AsciiJSON + +Using AsciiJSON to Generates ASCII-only JSON with escaped non-ASCII chracters. + +```go +func main() { + r := gin.Default() + + r.GET("/someJSON", func(c *gin.Context) { + data := map[string]interface{}{ + "lang": "GO语言", + "tag": "
", + } + + // will output : {"lang":"GO\u8bed\u8a00","tag":"\u003cbr\u003e"} + c.AsciiJSON(http.StatusOK, data) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +#### PureJSON + +Normally, JSON replaces special HTML characters with their unicode entities, e.g. `<` becomes `\u003c`. If you want to encode such characters literally, you can use PureJSON instead. +This feature is unavailable in Go 1.6 and lower. + +```go +func main() { + r := gin.Default() + + // Serves unicode entities + r.GET("/json", func(c *gin.Context) { + c.JSON(200, gin.H{ + "html": "Hello, world!", + }) + }) + + // Serves literal characters + r.GET("/purejson", func(c *gin.Context) { + c.PureJSON(200, gin.H{ + "html": "Hello, world!", + }) + }) + + // listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### 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 serve on 0.0.0.0:8080 + router.Run(":8080") +} +``` + +### Serving data from reader + +```go +func main() { + router := gin.Default() + router.GET("/someDataFromReader", func(c *gin.Context) { + response, err := http.Get("https://raw.githubusercontent.com/gin-gonic/logo/master/color.png") + if err != nil || response.StatusCode != http.StatusOK { + c.Status(http.StatusServiceUnavailable) + return + } + + reader := response.Body + contentLength := response.ContentLength + contentType := response.Header.Get("Content-Type") + + extraHeaders := map[string]string{ + "Content-Disposition": `attachment; filename="gopher.png"`, + } + + c.DataFromReader(http.StatusOK, contentLength, contentType, reader, extraHeaders) + }) + router.Run(":8080") +} +``` + +### HTML rendering + +Using LoadHTMLGlob() or LoadHTMLFiles() + +```go +func main() { + 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", + }) + }) + router.Run(":8080") +} +``` + +templates/index.tmpl + +```html + +

+ {{ .title }} +

+ +``` + +Using templates with same name in different directories + +```go +func main() { + router := gin.Default() + router.LoadHTMLGlob("templates/**/*") + router.GET("/posts/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "posts/index.tmpl", gin.H{ + "title": "Posts", + }) + }) + router.GET("/users/index", func(c *gin.Context) { + c.HTML(http.StatusOK, "users/index.tmpl", gin.H{ + "title": "Users", + }) + }) + router.Run(":8080") +} +``` + +templates/posts/index.tmpl + +```html +{{ define "posts/index.tmpl" }} +

+ {{ .title }} +

+

Using posts/index.tmpl

+ +{{ end }} +``` + +templates/users/index.tmpl + +```html +{{ define "users/index.tmpl" }} +

+ {{ .title }} +

+

Using users/index.tmpl

+ +{{ end }} +``` + +#### Custom Template renderer + +You can also use your own html template render + +```go +import "html/template" + +func main() { + router := gin.Default() + html := template.Must(template.ParseFiles("file1", "file2")) + router.SetHTMLTemplate(html) + router.Run(":8080") +} +``` + +#### Custom Delimiters + +You may use custom delims + +```go + r := gin.Default() + r.Delims("{[{", "}]}") + r.LoadHTMLGlob("/path/to/templates") +``` + +#### Custom Template Funcs + +See the detail [example code](https://github.com/gin-gonic/examples/tree/master/template). + +main.go + +```go +import ( + "fmt" + "html/template" + "net/http" + "time" + + "github.com/gin-gonic/gin" +) + +func formatAsDate(t time.Time) string { + year, month, day := t.Date() + return fmt.Sprintf("%d%02d/%02d", year, month, day) +} + +func main() { + router := gin.Default() + router.Delims("{[{", "}]}") + router.SetFuncMap(template.FuncMap{ + "formatAsDate": formatAsDate, + }) + router.LoadHTMLFiles("./testdata/template/raw.tmpl") + + router.GET("/raw", func(c *gin.Context) { + c.HTML(http.StatusOK, "raw.tmpl", map[string]interface{}{ + "now": time.Date(2017, 07, 01, 0, 0, 0, 0, time.UTC), + }) + }) + + router.Run(":8080") +} + +``` + +raw.tmpl + +```html +Date: {[{.now | formatAsDate}]} +``` + +Result: +``` +Date: 2017/07/01 +``` + +### Multitemplate + +Gin allow by default use only one html.Template. Check [a multitemplate render](https://github.com/gin-contrib/multitemplate) for using features like go 1.6 `block template`. + +### Redirects + +Issuing a HTTP redirect is easy. Both internal and external locations are supported. + +```go +r.GET("/test", func(c *gin.Context) { + c.Redirect(http.StatusMovedPermanently, "http://www.google.com/") +}) +``` + + +Issuing a Router redirect, use `HandleContext` like below. + +``` go +r.GET("/test", func(c *gin.Context) { + c.Request.URL.Path = "/test2" + r.HandleContext(c) +}) +r.GET("/test2", func(c *gin.Context) { + c.JSON(200, gin.H{"hello": "world"}) +}) +``` + + +### Custom Middleware + +```go +func Logger() gin.HandlerFunc { + return func(c *gin.Context) { + t := time.Now() + + // Set example variable + c.Set("example", "12345") + + // before request + + c.Next() + + // after request + latency := time.Since(t) + log.Print(latency) + + // access the status we are sending + status := c.Writer.Status() + log.Println(status) + } +} + +func main() { + r := gin.New() + r.Use(Logger()) + + r.GET("/test", func(c *gin.Context) { + example := c.MustGet("example").(string) + + // it would print: "12345" + log.Println(example) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### Using BasicAuth() middleware + +```go +// 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"}, + "lena": gin.H{"email": "lena@guapa.com", "phone": "523443"}, +} + +func main() { + r := gin.Default() + + // Group using gin.BasicAuth() middleware + // gin.Accounts is a shortcut for map[string]string + authorized := r.Group("/admin", gin.BasicAuth(gin.Accounts{ + "foo": "bar", + "austin": "1234", + "lena": "hello2", + "manu": "4321", + })) + + // /admin/secrets endpoint + // hit "localhost:8080/admin/secrets + authorized.GET("/secrets", func(c *gin.Context) { + // get user, it was set by the BasicAuth middleware + user := c.MustGet(gin.AuthUserKey).(string) + if secret, ok := secrets[user]; ok { + c.JSON(http.StatusOK, gin.H{"user": user, "secret": secret}) + } else { + c.JSON(http.StatusOK, gin.H{"user": user, "secret": "NO SECRET :("}) + } + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### Goroutines inside a middleware + +When starting new Goroutines inside a middleware or handler, you **SHOULD NOT** use the original context inside it, you have to use a read-only copy. + +```go +func main() { + r := gin.Default() + + r.GET("/long_async", func(c *gin.Context) { + // create copy to be used inside the goroutine + cCp := c.Copy() + go func() { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // note that you are using the copied context "cCp", IMPORTANT + log.Println("Done! in path " + cCp.Request.URL.Path) + }() + }) + + r.GET("/long_sync", func(c *gin.Context) { + // simulate a long task with time.Sleep(). 5 seconds + time.Sleep(5 * time.Second) + + // since we are NOT using a goroutine, we do not have to copy the context + log.Println("Done! in path " + c.Request.URL.Path) + }) + + // Listen and serve on 0.0.0.0:8080 + r.Run(":8080") +} +``` + +### Custom HTTP configuration + +Use `http.ListenAndServe()` directly, like this: + +```go +func main() { + router := gin.Default() + http.ListenAndServe(":8080", router) +} +``` +or + +```go +func main() { + router := gin.Default() + + s := &http.Server{ + Addr: ":8080", + Handler: router, + ReadTimeout: 10 * time.Second, + WriteTimeout: 10 * time.Second, + MaxHeaderBytes: 1 << 20, + } + s.ListenAndServe() +} +``` + +### Support Let's Encrypt + +example for 1-line LetsEncrypt HTTPS servers. + +```go +package main + +import ( + "log" + + "github.com/gin-gonic/autotls" + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + + // Ping handler + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + log.Fatal(autotls.Run(r, "example1.com", "example2.com")) +} +``` + +example for custom autocert manager. + +```go +package main + +import ( + "log" + + "github.com/gin-gonic/autotls" + "github.com/gin-gonic/gin" + "golang.org/x/crypto/acme/autocert" +) + +func main() { + r := gin.Default() + + // Ping handler + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + + m := autocert.Manager{ + Prompt: autocert.AcceptTOS, + HostPolicy: autocert.HostWhitelist("example1.com", "example2.com"), + Cache: autocert.DirCache("/var/www/.cache"), + } + + log.Fatal(autotls.RunWithManager(r, &m)) +} +``` + +### Run multiple service using Gin + +See the [question](https://github.com/gin-gonic/gin/issues/346) and try the following example: + +```go +package main + +import ( + "log" + "net/http" + "time" + + "github.com/gin-gonic/gin" + "golang.org/x/sync/errgroup" +) + +var ( + g errgroup.Group +) + +func router01() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 01", + }, + ) + }) + + return e +} + +func router02() http.Handler { + e := gin.New() + e.Use(gin.Recovery()) + e.GET("/", func(c *gin.Context) { + c.JSON( + http.StatusOK, + gin.H{ + "code": http.StatusOK, + "error": "Welcome server 02", + }, + ) + }) + + return e +} + +func main() { + server01 := &http.Server{ + Addr: ":8080", + Handler: router01(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + server02 := &http.Server{ + Addr: ":8081", + Handler: router02(), + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + } + + g.Go(func() error { + return server01.ListenAndServe() + }) + + g.Go(func() error { + return server02.ListenAndServe() + }) + + if err := g.Wait(); err != nil { + log.Fatal(err) + } +} +``` + +### Graceful restart or stop + +Do you want to graceful restart or stop your web server? +There are some ways this can be done. + +We can use [fvbock/endless](https://github.com/fvbock/endless) to replace the default `ListenAndServe`. Refer issue [#296](https://github.com/gin-gonic/gin/issues/296) for more details. + +```go +router := gin.Default() +router.GET("/", handler) +// [...] +endless.ListenAndServe(":4242", router) +``` + +An alternative to endless: + +* [manners](https://github.com/braintree/manners): A polite Go HTTP server that shuts down gracefully. +* [graceful](https://github.com/tylerb/graceful): Graceful is a Go package enabling graceful shutdown of an http.Handler server. +* [grace](https://github.com/facebookgo/grace): Graceful restart & zero downtime deploy for Go servers. + +If you are using Go 1.8, you may not need to use this library! Consider using http.Server's built-in [Shutdown()](https://golang.org/pkg/net/http/#Server.Shutdown) method for graceful shutdowns. See the full [graceful-shutdown](https://github.com/gin-gonic/examples/tree/master/graceful-shutdown) example with gin. + +```go +// +build go1.8 + +package main + +import ( + "context" + "log" + "net/http" + "os" + "os/signal" + "syscall" + "time" + + "github.com/gin-gonic/gin" +) + +func main() { + router := gin.Default() + router.GET("/", func(c *gin.Context) { + time.Sleep(5 * time.Second) + c.String(http.StatusOK, "Welcome Gin Server") + }) + + srv := &http.Server{ + Addr: ":8080", + Handler: router, + } + + go func() { + // service connections + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Fatalf("listen: %s\n", err) + } + }() + + // Wait for interrupt signal to gracefully shutdown the server with + // a timeout of 5 seconds. + quit := make(chan os.Signal) + // kill (no param) default send syscanll.SIGTERM + // kill -2 is syscall.SIGINT + // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it + signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) + <-quit + log.Println("Shutdown Server ...") + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(ctx); err != nil { + log.Fatal("Server Shutdown:", err) + } + // catching ctx.Done(). timeout of 5 seconds. + select { + case <-ctx.Done(): + log.Println("timeout of 5 seconds.") + } + log.Println("Server exiting") +} +``` + +### Build a single binary with templates + +You can build a server into a single binary containing templates by using [go-assets][]. + +[go-assets]: https://github.com/jessevdk/go-assets + +```go +func main() { + r := gin.New() + + t, err := loadTemplate() + if err != nil { + panic(err) + } + r.SetHTMLTemplate(t) + + r.GET("/", func(c *gin.Context) { + c.HTML(http.StatusOK, "/html/index.tmpl",nil) + }) + r.Run(":8080") +} + +// loadTemplate loads templates embedded by go-assets-builder +func loadTemplate() (*template.Template, error) { + t := template.New("") + for name, file := range Assets.Files { + if file.IsDir() || !strings.HasSuffix(name, ".tmpl") { + continue + } + h, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + t, err = t.New(name).Parse(string(h)) + if err != nil { + return nil, err + } + } + return t, nil +} +``` + +See a complete example in the `https://github.com/gin-gonic/examples/tree/master/assets-in-binary` directory. + +### Bind form-data request with custom struct + +The follow example using custom struct: + +```go +type StructA struct { + FieldA string `form:"field_a"` +} + +type StructB struct { + NestedStruct StructA + FieldB string `form:"field_b"` +} + +type StructC struct { + NestedStructPointer *StructA + FieldC string `form:"field_c"` +} + +type StructD struct { + NestedAnonyStruct struct { + FieldX string `form:"field_x"` + } + FieldD string `form:"field_d"` +} + +func GetDataB(c *gin.Context) { + var b StructB + c.Bind(&b) + c.JSON(200, gin.H{ + "a": b.NestedStruct, + "b": b.FieldB, + }) +} + +func GetDataC(c *gin.Context) { + var b StructC + c.Bind(&b) + c.JSON(200, gin.H{ + "a": b.NestedStructPointer, + "c": b.FieldC, + }) +} + +func GetDataD(c *gin.Context) { + var b StructD + c.Bind(&b) + c.JSON(200, gin.H{ + "x": b.NestedAnonyStruct, + "d": b.FieldD, + }) +} + +func main() { + r := gin.Default() + r.GET("/getb", GetDataB) + r.GET("/getc", GetDataC) + r.GET("/getd", GetDataD) + + r.Run() +} +``` + +Using the command `curl` command result: + +``` +$ curl "http://localhost:8080/getb?field_a=hello&field_b=world" +{"a":{"FieldA":"hello"},"b":"world"} +$ curl "http://localhost:8080/getc?field_a=hello&field_c=world" +{"a":{"FieldA":"hello"},"c":"world"} +$ curl "http://localhost:8080/getd?field_x=hello&field_d=world" +{"d":"world","x":{"FieldX":"hello"}} +``` + +### Try to bind body into different structs + +The normal methods for binding request body consumes `c.Request.Body` and they +cannot be called multiple times. + +```go +type formA struct { + Foo string `json:"foo" xml:"foo" binding:"required"` +} + +type formB struct { + Bar string `json:"bar" xml:"bar" binding:"required"` +} + +func SomeHandler(c *gin.Context) { + objA := formA{} + objB := formB{} + // This c.ShouldBind consumes c.Request.Body and it cannot be reused. + if errA := c.ShouldBind(&objA); errA == nil { + c.String(http.StatusOK, `the body should be formA`) + // Always an error is occurred by this because c.Request.Body is EOF now. + } else if errB := c.ShouldBind(&objB); errB == nil { + c.String(http.StatusOK, `the body should be formB`) + } else { + ... + } +} +``` + +For this, you can use `c.ShouldBindBodyWith`. + +```go +func SomeHandler(c *gin.Context) { + objA := formA{} + objB := formB{} + // This reads c.Request.Body and stores the result into the context. + if errA := c.ShouldBindBodyWith(&objA, binding.JSON); errA == nil { + c.String(http.StatusOK, `the body should be formA`) + // At this time, it reuses body stored in the context. + } else if errB := c.ShouldBindBodyWith(&objB, binding.JSON); errB == nil { + c.String(http.StatusOK, `the body should be formB JSON`) + // And it can accepts other formats + } else if errB2 := c.ShouldBindBodyWith(&objB, binding.XML); errB2 == nil { + c.String(http.StatusOK, `the body should be formB XML`) + } else { + ... + } +} +``` + +* `c.ShouldBindBodyWith` stores body into the context before binding. This has +a slight impact to performance, so you should not use this method if you are +enough to call binding at once. +* This feature is only needed for some formats -- `JSON`, `XML`, `MsgPack`, +`ProtoBuf`. For other formats, `Query`, `Form`, `FormPost`, `FormMultipart`, +can be called by `c.ShouldBind()` multiple times without any damage to +performance (See [#1341](https://github.com/gin-gonic/gin/pull/1341)). + +### http2 server push + +http.Pusher is supported only **go1.8+**. See the [golang blog](https://blog.golang.org/h2push) for detail information. + +```go +package main + +import ( + "html/template" + "log" + + "github.com/gin-gonic/gin" +) + +var html = template.Must(template.New("https").Parse(` + + + Https Test + + + +

Welcome, Ginner!

+ + +`)) + +func main() { + r := gin.Default() + r.Static("/assets", "./assets") + r.SetHTMLTemplate(html) + + r.GET("/", func(c *gin.Context) { + if pusher := c.Writer.Pusher(); pusher != nil { + // use pusher.Push() to do server push + if err := pusher.Push("/assets/app.js", nil); err != nil { + log.Printf("Failed to push: %v", err) + } + } + c.HTML(200, "https", gin.H{ + "status": "success", + }) + }) + + // Listen and Server in https://127.0.0.1:8080 + r.RunTLS(":8080", "./testdata/server.pem", "./testdata/server.key") +} +``` + +### Define format for the log of routes + +The default log of routes is: +``` +[GIN-debug] POST /foo --> main.main.func1 (3 handlers) +[GIN-debug] GET /bar --> main.main.func2 (3 handlers) +[GIN-debug] GET /status --> main.main.func3 (3 handlers) +``` + +If you want to log this information in given format (e.g. JSON, key values or something else), then you can define this format with `gin.DebugPrintRouteFunc`. +In the example below, we log all routes with standard log package but you can use another log tools that suits of your needs. +```go +import ( + "log" + "net/http" + + "github.com/gin-gonic/gin" +) + +func main() { + r := gin.Default() + gin.DebugPrintRouteFunc = func(httpMethod, absolutePath, handlerName string, nuHandlers int) { + log.Printf("endpoint %v %v %v %v\n", httpMethod, absolutePath, handlerName, nuHandlers) + } + + r.POST("/foo", func(c *gin.Context) { + c.JSON(http.StatusOK, "foo") + }) + + r.GET("/bar", func(c *gin.Context) { + c.JSON(http.StatusOK, "bar") + }) + + r.GET("/status", func(c *gin.Context) { + c.JSON(http.StatusOK, "ok") + }) + + // Listen and Server in http://0.0.0.0:8080 + r.Run() +} +``` + +### Set and get a cookie + +```go +import ( + "fmt" + + "github.com/gin-gonic/gin" +) + +func main() { + + router := gin.Default() + + router.GET("/cookie", func(c *gin.Context) { + + cookie, err := c.Cookie("gin_cookie") + + if err != nil { + cookie = "NotSet" + c.SetCookie("gin_cookie", "test", 3600, "/", "localhost", false, true) + } + + fmt.Printf("Cookie value: %s \n", cookie) + }) + + router.Run() +} +``` + + +## Testing + +The `net/http/httptest` package is preferable way for HTTP testing. + +```go +package main + +func setupRouter() *gin.Engine { + r := gin.Default() + r.GET("/ping", func(c *gin.Context) { + c.String(200, "pong") + }) + return r +} + +func main() { + r := setupRouter() + r.Run(":8080") +} +``` + +Test for code example above: + +```go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestPingRoute(t *testing.T) { + router := setupRouter() + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/ping", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, 200, w.Code) + assert.Equal(t, "pong", w.Body.String()) +} +``` ## Users -[Gin website](https://gin-gonic.com/docs/users/) lists some awesome projects made with Gin web framework. - -## Contributing - -Gin is the work of hundreds of contributors. We appreciate your help! - -Please see [CONTRIBUTING](CONTRIBUTING.md) for details on submitting patches and the contribution workflow. +Awesome project lists using [Gin](https://github.com/gin-gonic/gin) web framework. +* [gorush](https://github.com/appleboy/gorush): A push notification server written in Go. +* [fnproject](https://github.com/fnproject/fn): The container native, cloud agnostic serverless platform. +* [photoprism](https://github.com/photoprism/photoprism): Personal photo management powered by Go and Google TensorFlow. +* [krakend](https://github.com/devopsfaith/krakend): Ultra performant API Gateway with middlewares. +* [picfit](https://github.com/thoas/picfit): An image resizing server written in Go. From f9de6049cbf0820198708091e2b8e01696ec1473 Mon Sep 17 00:00:00 2001 From: Abhishek Chanda Date: Thu, 18 Apr 2019 03:45:37 +0100 Subject: [PATCH 06/10] Remove contents of the Authorization header while dumping requests (#1836) This PR replaces the contents of that header with a *. This prevents credential leak in logs. --- recovery.go | 9 ++++++++- recovery_test.go | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/recovery.go b/recovery.go index 9e893e1b..bc946c03 100644 --- a/recovery.go +++ b/recovery.go @@ -53,11 +53,18 @@ func RecoveryWithWriter(out io.Writer) HandlerFunc { if logger != nil { stack := stack(3) httpRequest, _ := httputil.DumpRequest(c.Request, false) + headers := strings.Split(string(httpRequest), "\r\n") + for idx, header := range headers { + current := strings.Split(header, ":") + if current[0] == "Authorization" { + headers[idx] = current[0] + ": *" + } + } if brokenPipe { logger.Printf("%s\n%s%s", err, string(httpRequest), reset) } else if IsDebugging() { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s\n%s%s", - timeFormat(time.Now()), string(httpRequest), err, stack, reset) + timeFormat(time.Now()), strings.Join(headers, "\r\n"), err, stack, reset) } else { logger.Printf("[Recovery] %s panic recovered:\n%s\n%s%s", timeFormat(time.Now()), err, stack, reset) diff --git a/recovery_test.go b/recovery_test.go index 0a6d6271..e1a0713f 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -8,6 +8,7 @@ package gin import ( "bytes" + "fmt" "net" "net/http" "os" @@ -18,6 +19,37 @@ import ( "github.com/stretchr/testify/assert" ) +func TestPanicClean(t *testing.T) { + buffer := new(bytes.Buffer) + router := New() + password := "my-super-secret-password" + router.Use(RecoveryWithWriter(buffer)) + router.GET("/recovery", func(c *Context) { + c.AbortWithStatus(http.StatusBadRequest) + panic("Oupps, Houston, we have a problem") + }) + // RUN + w := performRequest(router, "GET", "/recovery", + header{ + Key: "Host", + Value: "www.google.com", + }, + header{ + Key: "Authorization", + Value: fmt.Sprintf("Bearer %s", password), + }, + header{ + Key: "Content-Type", + Value: "application/json", + }, + ) + // TEST + assert.Equal(t, http.StatusBadRequest, w.Code) + + // Check the buffer does not have the secret key + assert.NotContains(t, buffer.String(), password) +} + // TestPanicInHandler assert that panic has been recovered. func TestPanicInHandler(t *testing.T) { buffer := new(bytes.Buffer) From 11407e73adb23e7ba4bf0fbdd02cc5336938a167 Mon Sep 17 00:00:00 2001 From: John Bampton Date: Tue, 23 Apr 2019 01:11:57 +1000 Subject: [PATCH 07/10] Fix spelling. (#1861) --- ginS/gins.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ginS/gins.go b/ginS/gins.go index 3ce4a6f6..3080fd34 100644 --- a/ginS/gins.go +++ b/ginS/gins.go @@ -118,7 +118,7 @@ func StaticFS(relativePath string, fs http.FileSystem) gin.IRoutes { return engine().StaticFS(relativePath, fs) } -// Use attachs a global middleware to the router. ie. the middlewares attached though Use() will be +// Use attaches a global middleware to the router. ie. the middlewares 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 Use(middlewares ...gin.HandlerFunc) gin.IRoutes { @@ -153,7 +153,7 @@ func RunUnix(file string) (err error) { // RunFd attaches the router to a http.Server and starts listening and serving HTTP requests // through the specified file descriptor. -// Note: thie method will block the calling goroutine indefinitely unless on error happens. +// Note: the method will block the calling goroutine indefinitely unless on error happens. func RunFd(fd int) (err error) { return engine().RunFd(fd) } From 202f8fc58af47ab5c8e834662ee7fc46deacc37d Mon Sep 17 00:00:00 2001 From: DeathKing Date: Wed, 24 Apr 2019 20:21:41 +0800 Subject: [PATCH 08/10] Fix a typo syscanll.SIGTERM -> syscall.SIGTERM (#1868) --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 804041f9..594c8bfa 100644 --- a/README.md +++ b/README.md @@ -1696,9 +1696,9 @@ func main() { // Wait for interrupt signal to gracefully shutdown the server with // a timeout of 5 seconds. quit := make(chan os.Signal) - // kill (no param) default send syscanll.SIGTERM + // kill (no param) default send syscall.SIGTERM // kill -2 is syscall.SIGINT - // kill -9 is syscall. SIGKILL but can"t be catch, so don't need add it + // kill -9 is syscall.SIGKILL but can"t be catch, so don't need add it signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM) <-quit log.Println("Shutdown Server ...") From 094f9a9105f8e7b971a01a64677eef5a1f7bcd9b Mon Sep 17 00:00:00 2001 From: Dan Markham Date: Tue, 7 May 2019 03:32:32 -0700 Subject: [PATCH 09/10] v1.4.0 + #1631 (remove go1.6/go1,7 support) (#1851) * remove go1.6 support * remove build tag * remove todo * remove go1.6 support: https://github.com/gin-gonic/gin/pull/1383/commits * update readme * remove go1.7 support * fix embedmd error * test * revert it * revert it * remove context_17 * add pusher test * v1.4.0 rc1 --- .travis.yml | 2 - CHANGELOG.md | 58 ++++++++++++++++++++++++++- README.md | 2 +- context.go | 19 ++++----- context_17.go | 17 -------- context_17_test.go | 27 ------------- context_test.go | 19 ++++++--- debug.go | 4 +- debug_test.go | 2 +- gin_integration_test.go | 37 ++++++++++++++++++ recovery_test.go | 2 - render/json.go | 18 +++++++++ render/json_17.go | 31 --------------- render/redirect.go | 4 +- render/render_17_test.go | 26 ------------- render/render_test.go | 12 ++++++ response_writer.go | 13 ++++++- response_writer_1.7.go | 12 ------ response_writer_1.8.go | 25 ------------ vendor/vendor.json | 84 +++++++++++++++++++++++++++++----------- version.go | 2 +- 21 files changed, 225 insertions(+), 191 deletions(-) delete mode 100644 context_17.go delete mode 100644 context_17_test.go delete mode 100644 render/json_17.go delete mode 100644 render/render_17_test.go delete mode 100644 response_writer_1.7.go delete mode 100644 response_writer_1.8.go diff --git a/.travis.yml b/.travis.yml index 2fd9c8a2..f6ec8a82 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,6 @@ language: go matrix: fast_finish: true include: - - go: 1.6.x - - go: 1.7.x - go: 1.8.x - go: 1.9.x - go: 1.10.x diff --git a/CHANGELOG.md b/CHANGELOG.md index e6a108ca..8ea2495d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,60 @@ -# CHANGELOG + +### Gin 1.4.0 + +- [NEW] Support for [Go Modules](https://github.com/golang/go/wiki/Modules) [#1569](https://github.com/gin-gonic/gin/pull/1569) +- [NEW] Refactor of form mapping multipart requesta [#1829](https://github.com/gin-gonic/gin/pull/1829) +- [FIX] Truncate Latency precision in long running request [#1830](https://github.com/gin-gonic/gin/pull/1830) +- [FIX] IsTerm flag should not be affected by DisableConsoleColor method. [#1802](https://github.com/gin-gonic/gin/pull/1802) +- [NEW] Supporting file binding [#1264](https://github.com/gin-gonic/gin/pull/1264) +- [NEW] Add support for mapping arrays [#1797](https://github.com/gin-gonic/gin/pull/1797) +- [FIX] Readme updates [#1793](https://github.com/gin-gonic/gin/pull/1793) [#1788](https://github.com/gin-gonic/gin/pull/1788) [1789](https://github.com/gin-gonic/gin/pull/1789) +- [FIX] StaticFS: Fixed Logging two log lines on 404. [#1805](https://github.com/gin-gonic/gin/pull/1805), [#1804](https://github.com/gin-gonic/gin/pull/1804) +- [NEW] Make context.Keys available as LogFormatterParams [#1779](https://github.com/gin-gonic/gin/pull/1779) +- [NEW] Use internal/json for Marshal/Unmarshal [#1791](https://github.com/gin-gonic/gin/pull/1791) +- [NEW] Support mapping time.Duration [#1794](https://github.com/gin-gonic/gin/pull/1794) +- [NEW] Refactor form mappings [#1749](https://github.com/gin-gonic/gin/pull/1749) +- [NEW] Added flag to context.Stream indicates if client disconnected in middle of stream [#1252](https://github.com/gin-gonic/gin/pull/1252) +- [FIX] Moved [examples](https://github.com/gin-gonic/examples) to stand alone Repo [#1775](https://github.com/gin-gonic/gin/pull/1775) +- [NEW] Extend context.File to allow for the content-dispositon attachments via a new method context.Attachment [#1260](https://github.com/gin-gonic/gin/pull/1260) +- [FIX] Support HTTP content negotiation wildcards [#1112](https://github.com/gin-gonic/gin/pull/1112) +- [NEW] Add prefix from X-Forwarded-Prefix in redirectTrailingSlash [#1238](https://github.com/gin-gonic/gin/pull/1238) +- [FIX] context.Copy() race condition [#1020](https://github.com/gin-gonic/gin/pull/1020) +- [NEW] Add context.HandlerNames() [#1729](https://github.com/gin-gonic/gin/pull/1729) +- [FIX] Change color methods to public in the defaultLogger. [#1771](https://github.com/gin-gonic/gin/pull/1771) +- [FIX] Update writeHeaders method to use http.Header.Set [#1722](https://github.com/gin-gonic/gin/pull/1722) +- [NEW] Add response size to LogFormatterParams [#1752](https://github.com/gin-gonic/gin/pull/1752) +- [NEW] Allow ignoring field on form mapping [#1733](https://github.com/gin-gonic/gin/pull/1733) +- [NEW] Add a function to force color in console output. [#1724](https://github.com/gin-gonic/gin/pull/1724) +- [FIX] Context.Next() - recheck len of handlers on every iteration. [#1745](https://github.com/gin-gonic/gin/pull/1745) +- [FIX] Fix all errcheck warnings [#1739](https://github.com/gin-gonic/gin/pull/1739) [#1653](https://github.com/gin-gonic/gin/pull/1653) +- [NEW] context: inherits context cancellation and deadline from http.Request context for Go>=1.7 [#1690](https://github.com/gin-gonic/gin/pull/1690) +- [NEW] Binding for URL Params [#1694](https://github.com/gin-gonic/gin/pull/1694) +- [NEW] Add LoggerWithFormatter method [#1677](https://github.com/gin-gonic/gin/pull/1677) +- [FIX] CI testing updates [#1671](https://github.com/gin-gonic/gin/pull/1671) [#1670](https://github.com/gin-gonic/gin/pull/1670) [#1682](https://github.com/gin-gonic/gin/pull/1682) [#1669](https://github.com/gin-gonic/gin/pull/1669) +- [FIX] StaticFS(): Send 404 when path does not exist [#1663](https://github.com/gin-gonic/gin/pull/1663) +- [FIX] Handle nil body for JSON binding [#1638](https://github.com/gin-gonic/gin/pull/1638) +- [FIX] Support bind uri param [#1612](https://github.com/gin-gonic/gin/pull/1612) +- [FIX] recovery: fix issue with syscall import on google app engine [#1640](https://github.com/gin-gonic/gin/pull/1640) +- [FIX] Make sure the debug log contains line breaks [#1650](https://github.com/gin-gonic/gin/pull/1650) +- [FIX] Panic stack trace being printed during recovery of broken pipe [#1089](https://github.com/gin-gonic/gin/pull/1089) [#1259](https://github.com/gin-gonic/gin/pull/1259) +- [NEW] RunFd method to run http.Server through a file descriptor [#1609](https://github.com/gin-gonic/gin/pull/1609) +- [NEW] Yaml binding support [#1618](https://github.com/gin-gonic/gin/pull/1618) +- [FIX] Pass MaxMultipartMemory when FormFile is called [#1600](https://github.com/gin-gonic/gin/pull/1600) +- [FIX] LoadHTML* tests [#1559](https://github.com/gin-gonic/gin/pull/1559) +- [FIX] Removed use of sync.pool from HandleContext [#1565](https://github.com/gin-gonic/gin/pull/1565) +- [FIX] Format output log to os.Stderr [#1571](https://github.com/gin-gonic/gin/pull/1571) +- [FIX] Make logger use a yellow background and a darkgray text for legibility [#1570](https://github.com/gin-gonic/gin/pull/1570) +- [FIX] Remove sensitive request information from panic log. [#1370](https://github.com/gin-gonic/gin/pull/1370) +- [FIX] log.Println() does not print timestamp [#829](https://github.com/gin-gonic/gin/pull/829) [#1560](https://github.com/gin-gonic/gin/pull/1560) +- [NEW] Add PureJSON renderer [#694](https://github.com/gin-gonic/gin/pull/694) +- [FIX] Add missing copyright and update if/else [#1497](https://github.com/gin-gonic/gin/pull/1497) +- [FIX] Update msgpack usage [#1498](https://github.com/gin-gonic/gin/pull/1498) +- [FIX] Use protobuf on render [#1496](https://github.com/gin-gonic/gin/pull/1496) +- [FIX] Add support for Protobuf format response [#1479](https://github.com/gin-gonic/gin/pull/1479) +- [NEW] Set default time format in form binding [#1487](https://github.com/gin-gonic/gin/pull/1487) +- [FIX] Add BindXML and ShouldBindXML [#1485](https://github.com/gin-gonic/gin/pull/1485) +- [NEW] Upgrade dependency libraries [#1491](https://github.com/gin-gonic/gin/pull/1491) + ### Gin 1.3.0 diff --git a/README.md b/README.md index 594c8bfa..3e817a78 100644 --- a/README.md +++ b/README.md @@ -69,7 +69,7 @@ Gin is a web framework written in Go (Golang). It features a martini-like API wi To install Gin package, you need to install Go and set your Go workspace first. -1. Download and install it: +1. The first need [Go](https://golang.org/) installed (**version 1.8+ is required**), then you can use the below Go command to install Gin. ```sh $ go get -u github.com/gin-gonic/gin diff --git a/context.go b/context.go index 5dc7f8a0..af747a1e 100644 --- a/context.go +++ b/context.go @@ -439,11 +439,6 @@ func (c *Context) GetPostFormArray(key string) ([]string, bool) { if values := req.PostForm[key]; len(values) > 0 { return values, true } - if req.MultipartForm != nil && req.MultipartForm.File != nil { - if values := req.MultipartForm.Value[key]; len(values) > 0 { - return values, true - } - } return []string{}, false } @@ -462,13 +457,7 @@ func (c *Context) GetPostFormMap(key string) (map[string]string, bool) { debugPrint("error on parse multipart form map: %v", err) } } - dicts, exist := c.get(req.PostForm, key) - - if !exist && req.MultipartForm != nil && req.MultipartForm.File != nil { - dicts, exist = c.get(req.MultipartForm.Value, key) - } - - return dicts, exist + return c.get(req.PostForm, key) } // get is an internal method and returns a map which satisfy conditions. @@ -828,6 +817,12 @@ func (c *Context) AsciiJSON(code int, obj interface{}) { c.Render(code, render.AsciiJSON{Data: obj}) } +// PureJSON serializes the given struct as JSON into the response body. +// PureJSON, unlike JSON, does not replace special html characters with their unicode entities. +func (c *Context) PureJSON(code int, obj interface{}) { + c.Render(code, render.PureJSON{Data: obj}) +} + // XML 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{}) { diff --git a/context_17.go b/context_17.go deleted file mode 100644 index 8e9f75ad..00000000 --- a/context_17.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 Gin Core Team. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -// +build go1.7 - -package gin - -import ( - "github.com/gin-gonic/gin/render" -) - -// PureJSON serializes the given struct as JSON into the response body. -// PureJSON, unlike JSON, does not replace special html characters with their unicode entities. -func (c *Context) PureJSON(code int, obj interface{}) { - c.Render(code, render.PureJSON{Data: obj}) -} diff --git a/context_17_test.go b/context_17_test.go deleted file mode 100644 index 5b9ebcdc..00000000 --- a/context_17_test.go +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright 2018 Gin Core Team. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -// +build go1.7 - -package gin - -import ( - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -// Tests that the response is serialized as JSON -// and Content-Type is set to application/json -// and special HTML characters are preserved -func TestContextRenderPureJSON(t *testing.T) { - w := httptest.NewRecorder() - c, _ := CreateTestContext(w) - c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": ""}) - assert.Equal(t, http.StatusCreated, w.Code) - assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) -} diff --git a/context_test.go b/context_test.go index 0da5fbe6..490e4490 100644 --- a/context_test.go +++ b/context_test.go @@ -622,8 +622,7 @@ func TestContextGetCookie(t *testing.T) { } func TestContextBodyAllowedForStatus(t *testing.T) { - // todo(thinkerou): go1.6 not support StatusProcessing - assert.False(t, false, bodyAllowedForStatus(102)) + assert.False(t, false, bodyAllowedForStatus(http.StatusProcessing)) assert.False(t, false, bodyAllowedForStatus(http.StatusNoContent)) assert.False(t, false, bodyAllowedForStatus(http.StatusNotModified)) assert.True(t, true, bodyAllowedForStatus(http.StatusInternalServerError)) @@ -794,6 +793,18 @@ func TestContextRenderNoContentAsciiJSON(t *testing.T) { assert.Equal(t, "application/json", w.Header().Get("Content-Type")) } +// Tests that the response is serialized as JSON +// and Content-Type is set to application/json +// and special HTML characters are preserved +func TestContextRenderPureJSON(t *testing.T) { + w := httptest.NewRecorder() + c, _ := CreateTestContext(w) + c.PureJSON(http.StatusCreated, H{"foo": "bar", "html": ""}) + assert.Equal(t, http.StatusCreated, w.Code) + assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) +} + // Tests that the response executes the templates // and responds with Content-Type set to text/html func TestContextRenderHTML(t *testing.T) { @@ -1092,9 +1103,7 @@ func TestContextRenderRedirectAll(t *testing.T) { assert.Panics(t, func() { c.Redirect(299, "/resource") }) assert.Panics(t, func() { c.Redirect(309, "/resource") }) assert.NotPanics(t, func() { c.Redirect(http.StatusMultipleChoices, "/resource") }) - // todo(thinkerou): go1.6 not support StatusPermanentRedirect(308) - // when we upgrade go version we can use http.StatusPermanentRedirect - assert.NotPanics(t, func() { c.Redirect(308, "/resource") }) + assert.NotPanics(t, func() { c.Redirect(http.StatusPermanentRedirect, "/resource") }) } func TestContextNegotiationWithJSON(t *testing.T) { diff --git a/debug.go b/debug.go index 98c67cf7..6d40a5da 100644 --- a/debug.go +++ b/debug.go @@ -14,7 +14,7 @@ import ( "strings" ) -const ginSupportMinGoVer = 6 +const ginSupportMinGoVer = 8 // IsDebugging returns true if the framework is running in debug mode. // Use SetMode(gin.ReleaseMode) to disable debug mode. @@ -69,7 +69,7 @@ func getMinVer(v string) (uint64, error) { func debugPrintWARNINGDefault() { if v, e := getMinVer(runtime.Version()); e == nil && v <= ginSupportMinGoVer { - debugPrint(`[WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon. + debugPrint(`[WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon. `) } diff --git a/debug_test.go b/debug_test.go index d338f0a0..86a67773 100644 --- a/debug_test.go +++ b/debug_test.go @@ -91,7 +91,7 @@ func TestDebugPrintWARNINGDefault(t *testing.T) { }) m, e := getMinVer(runtime.Version()) if e == nil && m <= ginSupportMinGoVer { - assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.6 or later and Go 1.7 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) + assert.Equal(t, "[GIN-debug] [WARNING] Now Gin requires Go 1.8 or later and Go 1.9 will be required soon.\n\n[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } else { assert.Equal(t, "[GIN-debug] [WARNING] Creating an Engine instance with the Logger and Recovery middleware already attached.\n\n", re) } diff --git a/gin_integration_test.go b/gin_integration_test.go index b80cbb24..9beec14d 100644 --- a/gin_integration_test.go +++ b/gin_integration_test.go @@ -8,6 +8,7 @@ import ( "bufio" "crypto/tls" "fmt" + "html/template" "io/ioutil" "net" "net/http" @@ -69,6 +70,42 @@ func TestRunTLS(t *testing.T) { testRequest(t, "https://localhost:8443/example") } +func TestPusher(t *testing.T) { + var html = template.Must(template.New("https").Parse(` + + + Https Test + + + +

Welcome, Ginner!

+ + +`)) + + router := New() + router.Static("./assets", "./assets") + router.SetHTMLTemplate(html) + + go func() { + router.GET("/pusher", func(c *Context) { + if pusher := c.Writer.Pusher(); pusher != nil { + pusher.Push("/assets/app.js", nil) + } + c.String(http.StatusOK, "it worked") + }) + + assert.NoError(t, router.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) + }() + + // 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.RunTLS(":8449", "./testdata/certificate/cert.pem", "./testdata/certificate/key.pem")) + testRequest(t, "https://localhost:8449/pusher") +} + func TestRunEmptyWithEnv(t *testing.T) { os.Setenv("PORT", "3123") router := New() diff --git a/recovery_test.go b/recovery_test.go index e1a0713f..21a0a480 100644 --- a/recovery_test.go +++ b/recovery_test.go @@ -2,8 +2,6 @@ // Use of this source code is governed by a MIT style // license that can be found in the LICENSE file. -// +build go1.7 - package gin import ( diff --git a/render/json.go b/render/json.go index c7cf330e..18f27fa9 100644 --- a/render/json.go +++ b/render/json.go @@ -43,6 +43,11 @@ type AsciiJSON struct { // SecureJSONPrefix is a string which represents SecureJSON prefix. type SecureJSONPrefix string +// PureJSON contains the given interface object. +type PureJSON struct { + Data interface{} +} + var jsonContentType = []string{"application/json; charset=utf-8"} var jsonpContentType = []string{"application/javascript; charset=utf-8"} var jsonAsciiContentType = []string{"application/json"} @@ -174,3 +179,16 @@ func (r AsciiJSON) Render(w http.ResponseWriter) (err error) { func (r AsciiJSON) WriteContentType(w http.ResponseWriter) { writeContentType(w, jsonAsciiContentType) } + +// Render (PureJSON) writes custom ContentType and encodes the given interface object. +func (r PureJSON) Render(w http.ResponseWriter) error { + r.WriteContentType(w) + encoder := json.NewEncoder(w) + encoder.SetEscapeHTML(false) + return encoder.Encode(r.Data) +} + +// WriteContentType (PureJSON) writes custom ContentType. +func (r PureJSON) WriteContentType(w http.ResponseWriter) { + writeContentType(w, jsonContentType) +} diff --git a/render/json_17.go b/render/json_17.go deleted file mode 100644 index 208193c7..00000000 --- a/render/json_17.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018 Gin Core Team. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -// +build go1.7 - -package render - -import ( - "net/http" - - "github.com/gin-gonic/gin/internal/json" -) - -// PureJSON contains the given interface object. -type PureJSON struct { - Data interface{} -} - -// Render (PureJSON) writes custom ContentType and encodes the given interface object. -func (r PureJSON) Render(w http.ResponseWriter) error { - r.WriteContentType(w) - encoder := json.NewEncoder(w) - encoder.SetEscapeHTML(false) - return encoder.Encode(r.Data) -} - -// WriteContentType (PureJSON) writes custom ContentType. -func (r PureJSON) WriteContentType(w http.ResponseWriter) { - writeContentType(w, jsonContentType) -} diff --git a/render/redirect.go b/render/redirect.go index 9c145fe2..c006691c 100644 --- a/render/redirect.go +++ b/render/redirect.go @@ -18,9 +18,7 @@ type Redirect struct { // Render (Redirect) redirects the http request to new location and writes redirect response. func (r Redirect) Render(w http.ResponseWriter) error { - // todo(thinkerou): go1.6 not support StatusPermanentRedirect(308) - // when we upgrade go version we can use http.StatusPermanentRedirect - if (r.Code < 300 || r.Code > 308) && r.Code != 201 { + if (r.Code < http.StatusMultipleChoices || r.Code > http.StatusPermanentRedirect) && r.Code != http.StatusCreated { panic(fmt.Sprintf("Cannot redirect with status code %d", r.Code)) } http.Redirect(w, r.Request, r.Location, r.Code) diff --git a/render/render_17_test.go b/render/render_17_test.go deleted file mode 100644 index 68330090..00000000 --- a/render/render_17_test.go +++ /dev/null @@ -1,26 +0,0 @@ -// Copyright 2018 Gin Core Team. All rights reserved. -// Use of this source code is governed by a MIT style -// license that can be found in the LICENSE file. - -// +build go1.7 - -package render - -import ( - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestRenderPureJSON(t *testing.T) { - w := httptest.NewRecorder() - data := map[string]interface{}{ - "foo": "bar", - "html": "", - } - err := (PureJSON{data}).Render(w) - assert.NoError(t, err) - assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String()) - assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) -} diff --git a/render/render_test.go b/render/render_test.go index 76e29eeb..3aa5dbcc 100644 --- a/render/render_test.go +++ b/render/render_test.go @@ -215,6 +215,18 @@ func TestRenderAsciiJSONFail(t *testing.T) { assert.Error(t, (AsciiJSON{data}).Render(w)) } +func TestRenderPureJSON(t *testing.T) { + w := httptest.NewRecorder() + data := map[string]interface{}{ + "foo": "bar", + "html": "", + } + err := (PureJSON{data}).Render(w) + assert.NoError(t, err) + assert.Equal(t, "{\"foo\":\"bar\",\"html\":\"\"}\n", w.Body.String()) + assert.Equal(t, "application/json; charset=utf-8", w.Header().Get("Content-Type")) +} + type xmlmap map[string]interface{} // Allows type H to be used with xml.Marshal diff --git a/response_writer.go b/response_writer.go index 923b53f8..26826689 100644 --- a/response_writer.go +++ b/response_writer.go @@ -16,7 +16,8 @@ const ( defaultStatus = http.StatusOK ) -type responseWriterBase interface { +// ResponseWriter ... +type ResponseWriter interface { http.ResponseWriter http.Hijacker http.Flusher @@ -37,6 +38,9 @@ type responseWriterBase interface { // Forces to write the http header (status code + headers). WriteHeaderNow() + + // get the http.Pusher for server push + Pusher() http.Pusher } type responseWriter struct { @@ -113,3 +117,10 @@ func (w *responseWriter) Flush() { w.WriteHeaderNow() w.ResponseWriter.(http.Flusher).Flush() } + +func (w *responseWriter) Pusher() (pusher http.Pusher) { + if pusher, ok := w.ResponseWriter.(http.Pusher); ok { + return pusher + } + return nil +} diff --git a/response_writer_1.7.go b/response_writer_1.7.go deleted file mode 100644 index 801d196b..00000000 --- a/response_writer_1.7.go +++ /dev/null @@ -1,12 +0,0 @@ -// +build !go1.8 - -// Copyright 2018 Gin Core Team. 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 - -// ResponseWriter ... -type ResponseWriter interface { - responseWriterBase -} diff --git a/response_writer_1.8.go b/response_writer_1.8.go deleted file mode 100644 index 527c0038..00000000 --- a/response_writer_1.8.go +++ /dev/null @@ -1,25 +0,0 @@ -// +build go1.8 - -// Copyright 2018 Gin Core Team. 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" -) - -// ResponseWriter ... -type ResponseWriter interface { - responseWriterBase - // get the http.Pusher for server push - Pusher() http.Pusher -} - -func (w *responseWriter) Pusher() (pusher http.Pusher) { - if pusher, ok := w.ResponseWriter.(http.Pusher); ok { - return pusher - } - return nil -} diff --git a/vendor/vendor.json b/vendor/vendor.json index 6050e8f6..fc7bb11d 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -1,5 +1,5 @@ { - "comment": "v1.3.0", + "comment": "v1.4.0", "ignore": "test", "package": [ { @@ -13,16 +13,16 @@ { "checksumSHA1": "QeKwBtN2df+j+4stw3bQJ6yO4EY=", "path": "github.com/gin-contrib/sse", - "revision": "22d885f9ecc78bf4ee5d72b937e4bbcdc58e8cae", - "revisionTime": "2017-01-09T09:34:21Z" + "revision": "5545eab6dad3bbbd6c5ae9186383c2a9d23c0dae", + "revisionTime": "2019-03-01T06:25:29Z" }, { - "checksumSHA1": "mE9XW26JSpe4meBObM6J/Oeq0eg=", + "checksumSHA1": "Y2MOwzNZfl4NRNDbLCZa6sgx7O0=", "path": "github.com/golang/protobuf/proto", - "revision": "aa810b61a9c79d51363740d207bb46cf8e620ed5", - "revisionTime": "2018-08-14T21:14:27Z", - "version": "v1.2", - "versionExact": "v1.2.0" + "revision": "c823c79ea1570fb5ff454033735a8e68575d1d0f", + "revisionTime": "2019-02-05T22:20:52Z", + "version": "v1.3", + "versionExact": "v1.3.0" }, { "checksumSHA1": "WqeEgS7pqqkwK8mlrAZmDgtWJMY=", @@ -33,12 +33,24 @@ "versionExact": "v1.1.5" }, { - "checksumSHA1": "w5RcOnfv5YDr3j2bd1YydkPiZx4=", + "checksumSHA1": "rrXDDvz+nQ2KRLQk6nxWaE5Zj1U=", "path": "github.com/mattn/go-isatty", - "revision": "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c", - "revisionTime": "2017-11-07T05:05:31Z", + "revision": "369ecd8cea9851e459abb67eb171853e3986591e", + "revisionTime": "2019-02-25T17:38:24Z", "version": "v0.0", - "versionExact": "v0.0.4" + "versionExact": "v0.0.6" + }, + { + "checksumSHA1": "ZTcgWKWHsrX0RXYVXn5Xeb8Q0go=", + "path": "github.com/modern-go/concurrent", + "revision": "bacd9c7ef1dd9b15be4a9909b8ac7a4e313eec94", + "revisionTime": "2018-03-06T01:26:44Z" + }, + { + "checksumSHA1": "qvH48wzTIV3QKSDqI0dLFtVjaDI=", + "path": "github.com/modern-go/reflect2", + "revision": "94122c33edd36123c84d5368cfb2b69df93a0ec8", + "revisionTime": "2018-07-18T01:23:57Z" }, { "checksumSHA1": "LuFv4/jlrmFNnDb/5SCSEPAM9vU=", @@ -46,6 +58,20 @@ "revision": "5d4384ee4fb2527b0a1256a821ebfc92f91efefc", "revisionTime": "2018-12-26T10:54:42Z" }, + { + "checksumSHA1": "cpNsoLqBprpKh+VZTBOZNVXzBEk=", + "path": "github.com/stretchr/objx", + "revision": "c61a9dfcced1815e7d40e214d00d1a8669a9f58c", + "revisionTime": "2019-02-11T16:23:28Z" + }, + { + "checksumSHA1": "DBdcVxnvaINHhWyyGgih/Mel6gE=", + "path": "github.com/stretchr/testify", + "revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053", + "revisionTime": "2018-12-05T02:12:43Z", + "version": "v1.3", + "versionExact": "v1.3.0" + }, { "checksumSHA1": "c6pbpF7eowwO59phRTpF8cQ80Z0=", "path": "github.com/stretchr/testify/assert", @@ -55,12 +81,26 @@ "versionExact": "v1.2.2" }, { - "checksumSHA1": "5Bd8RPhhaKcEXkagzPqymP4Gx5E=", + "checksumSHA1": "fg3TzS9/QK3wZbzei3Z6O8XPLHg=", + "path": "github.com/stretchr/testify/http", + "revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053", + "revisionTime": "2018-12-05T02:12:43Z", + "version": "v1.3", + "versionExact": "v1.3.0" + }, + { + "checksumSHA1": "lsdl3fgOiM4Iuy7xjTQxiBtAwB0=", + "path": "github.com/stretchr/testify/mock", + "revision": "ffdc059bfe9ce6a4e144ba849dbedead332c6053", + "revisionTime": "2018-12-05T02:12:43Z", + "version": "v1.3", + "versionExact": "v1.3.0" + }, + { + "checksumSHA1": "WIhpR3EKGueRSJsYOZ6PIsfL4SI=", "path": "github.com/ugorji/go/codec", - "revision": "b4c50a2b199d93b13dc15e78929cfb23bfdf21ab", - "revisionTime": "2018-04-07T10:07:33Z", - "version": "v1.1", - "versionExact": "v1.1.1" + "revision": "e444a5086c436778cf9281a7059a3d58b9e17935", + "revisionTime": "2019-02-04T20:13:41Z" }, { "checksumSHA1": "GtamqiJoL7PGHsN454AoffBFMa8=", @@ -83,13 +123,13 @@ "versionExact": "v8.18.2" }, { - "checksumSHA1": "ZSWoOPUNRr5+3dhkLK3C4cZAQPk=", + "checksumSHA1": "QqDq2x8XOU7IoOR98Cx1eiV5QY8=", "path": "gopkg.in/yaml.v2", - "revision": "5420a8b6744d3b0345ab293f6fcba19c978f1183", - "revisionTime": "2018-03-28T19:50:20Z", + "revision": "51d6538a90f86fe93ac480b35f37b2be17fef232", + "revisionTime": "2018-11-15T11:05:04Z", "version": "v2.2", - "versionExact": "v2.2.1" + "versionExact": "v2.2.2" } ], "rootPath": "github.com/gin-gonic/gin" -} +} \ No newline at end of file diff --git a/version.go b/version.go index 028caebe..07e7859f 100644 --- a/version.go +++ b/version.go @@ -5,4 +5,4 @@ package gin // Version is the current gin framework's version. -const Version = "v1.4.0-dev" +const Version = "v1.4.0" From 66d2c30c54ff8042f5ae13d9ebb26dfe556561fe Mon Sep 17 00:00:00 2001 From: Dmitry Kutakov Date: Tue, 7 May 2019 14:06:55 +0300 Subject: [PATCH 10/10] binding: move tests of mapping to separate test file (#1842) * move tests of mapping to separate test file make 100% coverage of form_mapping.go from form_mapping_test.go file * fix tests for go 1.6 go 1.6 doesn't support `t.Run(...)` subtests --- binding/binding_test.go | 406 ----------------------------------- binding/form_mapping_test.go | 271 +++++++++++++++++++++++ 2 files changed, 271 insertions(+), 406 deletions(-) create mode 100644 binding/form_mapping_test.go diff --git a/binding/binding_test.go b/binding/binding_test.go index ee788225..73bb7700 100644 --- a/binding/binding_test.go +++ b/binding/binding_test.go @@ -114,71 +114,6 @@ type FooStructForBoolType struct { BoolFoo bool `form:"bool_foo"` } -type FooBarStructForIntType struct { - IntFoo int `form:"int_foo"` - IntBar int `form:"int_bar" binding:"required"` -} - -type FooBarStructForInt8Type struct { - Int8Foo int8 `form:"int8_foo"` - Int8Bar int8 `form:"int8_bar" binding:"required"` -} - -type FooBarStructForInt16Type struct { - Int16Foo int16 `form:"int16_foo"` - Int16Bar int16 `form:"int16_bar" binding:"required"` -} - -type FooBarStructForInt32Type struct { - Int32Foo int32 `form:"int32_foo"` - Int32Bar int32 `form:"int32_bar" binding:"required"` -} - -type FooBarStructForInt64Type struct { - Int64Foo int64 `form:"int64_foo"` - Int64Bar int64 `form:"int64_bar" binding:"required"` -} - -type FooBarStructForUintType struct { - UintFoo uint `form:"uint_foo"` - UintBar uint `form:"uint_bar" binding:"required"` -} - -type FooBarStructForUint8Type struct { - Uint8Foo uint8 `form:"uint8_foo"` - Uint8Bar uint8 `form:"uint8_bar" binding:"required"` -} - -type FooBarStructForUint16Type struct { - Uint16Foo uint16 `form:"uint16_foo"` - Uint16Bar uint16 `form:"uint16_bar" binding:"required"` -} - -type FooBarStructForUint32Type struct { - Uint32Foo uint32 `form:"uint32_foo"` - Uint32Bar uint32 `form:"uint32_bar" binding:"required"` -} - -type FooBarStructForUint64Type struct { - Uint64Foo uint64 `form:"uint64_foo"` - Uint64Bar uint64 `form:"uint64_bar" binding:"required"` -} - -type FooBarStructForBoolType struct { - BoolFoo bool `form:"bool_foo"` - BoolBar bool `form:"bool_bar" binding:"required"` -} - -type FooBarStructForFloat32Type struct { - Float32Foo float32 `form:"float32_foo"` - Float32Bar float32 `form:"float32_bar" binding:"required"` -} - -type FooBarStructForFloat64Type struct { - Float64Foo float64 `form:"float64_foo"` - Float64Bar float64 `form:"float64_bar" binding:"required"` -} - type FooStructForStringPtrType struct { PtrFoo *string `form:"ptr_foo"` PtrBar *string `form:"ptr_bar" binding:"required"` @@ -335,110 +270,6 @@ func TestBindingFormForType(t *testing.T) { "/?slice_map_foo=1&slice_map_foo=2", "/?bar2=1&bar2=2", "", "", "SliceMap") - testFormBindingForType(t, "POST", - "/", "/", - "int_foo=&int_bar=-12", "bar2=-123", "Int") - - testFormBindingForType(t, "GET", - "/?int_foo=&int_bar=-12", "/?bar2=-123", - "", "", "Int") - - testFormBindingForType(t, "POST", - "/", "/", - "int8_foo=&int8_bar=-12", "bar2=-123", "Int8") - - testFormBindingForType(t, "GET", - "/?int8_foo=&int8_bar=-12", "/?bar2=-123", - "", "", "Int8") - - testFormBindingForType(t, "POST", - "/", "/", - "int16_foo=&int16_bar=-12", "bar2=-123", "Int16") - - testFormBindingForType(t, "GET", - "/?int16_foo=&int16_bar=-12", "/?bar2=-123", - "", "", "Int16") - - testFormBindingForType(t, "POST", - "/", "/", - "int32_foo=&int32_bar=-12", "bar2=-123", "Int32") - - testFormBindingForType(t, "GET", - "/?int32_foo=&int32_bar=-12", "/?bar2=-123", - "", "", "Int32") - - testFormBindingForType(t, "POST", - "/", "/", - "int64_foo=&int64_bar=-12", "bar2=-123", "Int64") - - testFormBindingForType(t, "GET", - "/?int64_foo=&int64_bar=-12", "/?bar2=-123", - "", "", "Int64") - - testFormBindingForType(t, "POST", - "/", "/", - "uint_foo=&uint_bar=12", "bar2=123", "Uint") - - testFormBindingForType(t, "GET", - "/?uint_foo=&uint_bar=12", "/?bar2=123", - "", "", "Uint") - - testFormBindingForType(t, "POST", - "/", "/", - "uint8_foo=&uint8_bar=12", "bar2=123", "Uint8") - - testFormBindingForType(t, "GET", - "/?uint8_foo=&uint8_bar=12", "/?bar2=123", - "", "", "Uint8") - - testFormBindingForType(t, "POST", - "/", "/", - "uint16_foo=&uint16_bar=12", "bar2=123", "Uint16") - - testFormBindingForType(t, "GET", - "/?uint16_foo=&uint16_bar=12", "/?bar2=123", - "", "", "Uint16") - - testFormBindingForType(t, "POST", - "/", "/", - "uint32_foo=&uint32_bar=12", "bar2=123", "Uint32") - - testFormBindingForType(t, "GET", - "/?uint32_foo=&uint32_bar=12", "/?bar2=123", - "", "", "Uint32") - - testFormBindingForType(t, "POST", - "/", "/", - "uint64_foo=&uint64_bar=12", "bar2=123", "Uint64") - - testFormBindingForType(t, "GET", - "/?uint64_foo=&uint64_bar=12", "/?bar2=123", - "", "", "Uint64") - - testFormBindingForType(t, "POST", - "/", "/", - "bool_foo=&bool_bar=true", "bar2=true", "Bool") - - testFormBindingForType(t, "GET", - "/?bool_foo=&bool_bar=true", "/?bar2=true", - "", "", "Bool") - - testFormBindingForType(t, "POST", - "/", "/", - "float32_foo=&float32_bar=-12.34", "bar2=12.3", "Float32") - - testFormBindingForType(t, "GET", - "/?float32_foo=&float32_bar=-12.34", "/?bar2=12.3", - "", "", "Float32") - - testFormBindingForType(t, "POST", - "/", "/", - "float64_foo=&float64_bar=-12.34", "bar2=12.3", "Float64") - - testFormBindingForType(t, "GET", - "/?float64_foo=&float64_bar=-12.34", "/?bar2=12.3", - "", "", "Float64") - testFormBindingForType(t, "POST", "/", "/", "ptr_bar=test", "bar2=test", "Ptr") @@ -1076,149 +907,6 @@ func testFormBindingForType(t *testing.T, method, path, badPath, body, badBody s req.Header.Add("Content-Type", MIMEPOSTForm) } switch typ { - case "Int": - obj := FooBarStructForIntType{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, int(0), obj.IntFoo) - assert.Equal(t, int(-12), obj.IntBar) - - obj = FooBarStructForIntType{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Int8": - obj := FooBarStructForInt8Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, int8(0), obj.Int8Foo) - assert.Equal(t, int8(-12), obj.Int8Bar) - - obj = FooBarStructForInt8Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Int16": - obj := FooBarStructForInt16Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, int16(0), obj.Int16Foo) - assert.Equal(t, int16(-12), obj.Int16Bar) - - obj = FooBarStructForInt16Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Int32": - obj := FooBarStructForInt32Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, int32(0), obj.Int32Foo) - assert.Equal(t, int32(-12), obj.Int32Bar) - - obj = FooBarStructForInt32Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Int64": - obj := FooBarStructForInt64Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, int64(0), obj.Int64Foo) - assert.Equal(t, int64(-12), obj.Int64Bar) - - obj = FooBarStructForInt64Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Uint": - obj := FooBarStructForUintType{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, uint(0x0), obj.UintFoo) - assert.Equal(t, uint(0xc), obj.UintBar) - - obj = FooBarStructForUintType{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Uint8": - obj := FooBarStructForUint8Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, uint8(0x0), obj.Uint8Foo) - assert.Equal(t, uint8(0xc), obj.Uint8Bar) - - obj = FooBarStructForUint8Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Uint16": - obj := FooBarStructForUint16Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, uint16(0x0), obj.Uint16Foo) - assert.Equal(t, uint16(0xc), obj.Uint16Bar) - - obj = FooBarStructForUint16Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Uint32": - obj := FooBarStructForUint32Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, uint32(0x0), obj.Uint32Foo) - assert.Equal(t, uint32(0xc), obj.Uint32Bar) - - obj = FooBarStructForUint32Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Uint64": - obj := FooBarStructForUint64Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, uint64(0x0), obj.Uint64Foo) - assert.Equal(t, uint64(0xc), obj.Uint64Bar) - - obj = FooBarStructForUint64Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Float32": - obj := FooBarStructForFloat32Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, float32(0.0), obj.Float32Foo) - assert.Equal(t, float32(-12.34), obj.Float32Bar) - - obj = FooBarStructForFloat32Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Float64": - obj := FooBarStructForFloat64Type{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.Equal(t, float64(0.0), obj.Float64Foo) - assert.Equal(t, float64(-12.34), obj.Float64Bar) - - obj = FooBarStructForFloat64Type{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) - case "Bool": - obj := FooBarStructForBoolType{} - err := b.Bind(req, &obj) - assert.NoError(t, err) - assert.False(t, obj.BoolFoo) - assert.True(t, obj.BoolBar) - - obj = FooBarStructForBoolType{} - req = requestWithBody(method, badPath, badBody) - err = JSON.Bind(req, &obj) - assert.Error(t, err) case "Slice": obj := FooStructForSliceType{} err := b.Bind(req, &obj) @@ -1454,97 +1142,3 @@ func requestWithBody(method, path, body string) (req *http.Request) { req, _ = http.NewRequest(method, path, bytes.NewBufferString(body)) return } - -func TestCanSet(t *testing.T) { - type CanSetStruct struct { - lowerStart string `form:"lower"` - } - - var c CanSetStruct - assert.Nil(t, mapForm(&c, nil)) -} - -func formPostRequest(path, body string) *http.Request { - req := requestWithBody("POST", path, body) - req.Header.Add("Content-Type", MIMEPOSTForm) - return req -} - -func TestBindingSliceDefault(t *testing.T) { - var s struct { - Friends []string `form:"friends,default=mike"` - } - req := formPostRequest("", "") - err := Form.Bind(req, &s) - assert.NoError(t, err) - - assert.Len(t, s.Friends, 1) - assert.Equal(t, "mike", s.Friends[0]) -} - -func TestBindingStructField(t *testing.T) { - var s struct { - Opts struct { - Port int - } `form:"opts"` - } - req := formPostRequest("", `opts={"Port": 8000}`) - err := Form.Bind(req, &s) - assert.NoError(t, err) - assert.Equal(t, 8000, s.Opts.Port) -} - -func TestBindingUnknownTypeChan(t *testing.T) { - var s struct { - Stop chan bool `form:"stop"` - } - req := formPostRequest("", "stop=true") - err := Form.Bind(req, &s) - assert.Error(t, err) - assert.Equal(t, errUnknownType, err) -} - -func TestBindingTimeDuration(t *testing.T) { - var s struct { - Timeout time.Duration `form:"timeout"` - } - - // ok - req := formPostRequest("", "timeout=5s") - err := Form.Bind(req, &s) - assert.NoError(t, err) - assert.Equal(t, 5*time.Second, s.Timeout) - - // error - req = formPostRequest("", "timeout=wrong") - err = Form.Bind(req, &s) - assert.Error(t, err) -} - -func TestBindingArray(t *testing.T) { - var s struct { - Nums [2]int `form:"nums,default=4"` - } - - // default - req := formPostRequest("", "") - err := Form.Bind(req, &s) - assert.Error(t, err) - assert.Equal(t, [2]int{0, 0}, s.Nums) - - // ok - req = formPostRequest("", "nums=3&nums=8") - err = Form.Bind(req, &s) - assert.NoError(t, err) - assert.Equal(t, [2]int{3, 8}, s.Nums) - - // not enough vals - req = formPostRequest("", "nums=3") - err = Form.Bind(req, &s) - assert.Error(t, err) - - // error - req = formPostRequest("", "nums=3&nums=wrong") - err = Form.Bind(req, &s) - assert.Error(t, err) -} diff --git a/binding/form_mapping_test.go b/binding/form_mapping_test.go new file mode 100644 index 00000000..c9d6111b --- /dev/null +++ b/binding/form_mapping_test.go @@ -0,0 +1,271 @@ +// Copyright 2019 Gin Core Team. 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 ( + "reflect" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestMappingBaseTypes(t *testing.T) { + intPtr := func(i int) *int { + return &i + } + for _, tt := range []struct { + name string + value interface{} + form string + expect interface{} + }{ + {"base type", struct{ F int }{}, "9", int(9)}, + {"base type", struct{ F int8 }{}, "9", int8(9)}, + {"base type", struct{ F int16 }{}, "9", int16(9)}, + {"base type", struct{ F int32 }{}, "9", int32(9)}, + {"base type", struct{ F int64 }{}, "9", int64(9)}, + {"base type", struct{ F uint }{}, "9", uint(9)}, + {"base type", struct{ F uint8 }{}, "9", uint8(9)}, + {"base type", struct{ F uint16 }{}, "9", uint16(9)}, + {"base type", struct{ F uint32 }{}, "9", uint32(9)}, + {"base type", struct{ F uint64 }{}, "9", uint64(9)}, + {"base type", struct{ F bool }{}, "True", true}, + {"base type", struct{ F float32 }{}, "9.1", float32(9.1)}, + {"base type", struct{ F float64 }{}, "9.1", float64(9.1)}, + {"base type", struct{ F string }{}, "test", string("test")}, + {"base type", struct{ F *int }{}, "9", intPtr(9)}, + + // zero values + {"zero value", struct{ F int }{}, "", int(0)}, + {"zero value", struct{ F uint }{}, "", uint(0)}, + {"zero value", struct{ F bool }{}, "", false}, + {"zero value", struct{ F float32 }{}, "", float32(0)}, + } { + tp := reflect.TypeOf(tt.value) + testName := tt.name + ":" + tp.Field(0).Type.String() + + val := reflect.New(reflect.TypeOf(tt.value)) + val.Elem().Set(reflect.ValueOf(tt.value)) + + field := val.Elem().Type().Field(0) + + _, err := mapping(val, emptyField, formSource{field.Name: {tt.form}}, "form") + assert.NoError(t, err, testName) + + actual := val.Elem().Field(0).Interface() + assert.Equal(t, tt.expect, actual, testName) + } +} + +func TestMappingDefault(t *testing.T) { + var s struct { + Int int `form:",default=9"` + Slice []int `form:",default=9"` + Array [1]int `form:",default=9"` + } + err := mappingByPtr(&s, formSource{}, "form") + assert.NoError(t, err) + + assert.Equal(t, 9, s.Int) + assert.Equal(t, []int{9}, s.Slice) + assert.Equal(t, [1]int{9}, s.Array) +} + +func TestMappingSkipField(t *testing.T) { + var s struct { + A int + } + err := mappingByPtr(&s, formSource{}, "form") + assert.NoError(t, err) + + assert.Equal(t, 0, s.A) +} + +func TestMappingIgnoreField(t *testing.T) { + var s struct { + A int `form:"A"` + B int `form:"-"` + } + err := mappingByPtr(&s, formSource{"A": {"9"}, "B": {"9"}}, "form") + assert.NoError(t, err) + + assert.Equal(t, 9, s.A) + assert.Equal(t, 0, s.B) +} + +func TestMappingUnexportedField(t *testing.T) { + var s struct { + A int `form:"a"` + b int `form:"b"` + } + err := mappingByPtr(&s, formSource{"a": {"9"}, "b": {"9"}}, "form") + assert.NoError(t, err) + + assert.Equal(t, 9, s.A) + assert.Equal(t, 0, s.b) +} + +func TestMappingPrivateField(t *testing.T) { + var s struct { + f int `form:"field"` + } + err := mappingByPtr(&s, formSource{"field": {"6"}}, "form") + assert.NoError(t, err) + assert.Equal(t, int(0), s.f) +} + +func TestMappingUnknownFieldType(t *testing.T) { + var s struct { + U uintptr + } + + err := mappingByPtr(&s, formSource{"U": {"unknown"}}, "form") + assert.Error(t, err) + assert.Equal(t, errUnknownType, err) +} + +func TestMappingURI(t *testing.T) { + var s struct { + F int `uri:"field"` + } + err := mapUri(&s, map[string][]string{"field": {"6"}}) + assert.NoError(t, err) + assert.Equal(t, int(6), s.F) +} + +func TestMappingForm(t *testing.T) { + var s struct { + F int `form:"field"` + } + err := mapForm(&s, map[string][]string{"field": {"6"}}) + assert.NoError(t, err) + assert.Equal(t, int(6), s.F) +} + +func TestMappingTime(t *testing.T) { + var s struct { + Time time.Time + LocalTime time.Time `time_format:"2006-01-02"` + ZeroValue time.Time + CSTTime time.Time `time_format:"2006-01-02" time_location:"Asia/Shanghai"` + UTCTime time.Time `time_format:"2006-01-02" time_utc:"1"` + } + + var err error + time.Local, err = time.LoadLocation("Europe/Berlin") + assert.NoError(t, err) + + err = mapForm(&s, map[string][]string{ + "Time": {"2019-01-20T16:02:58Z"}, + "LocalTime": {"2019-01-20"}, + "ZeroValue": {}, + "CSTTime": {"2019-01-20"}, + "UTCTime": {"2019-01-20"}, + }) + assert.NoError(t, err) + + assert.Equal(t, "2019-01-20 16:02:58 +0000 UTC", s.Time.String()) + assert.Equal(t, "2019-01-20 00:00:00 +0100 CET", s.LocalTime.String()) + assert.Equal(t, "2019-01-19 23:00:00 +0000 UTC", s.LocalTime.UTC().String()) + assert.Equal(t, "0001-01-01 00:00:00 +0000 UTC", s.ZeroValue.String()) + assert.Equal(t, "2019-01-20 00:00:00 +0800 CST", s.CSTTime.String()) + assert.Equal(t, "2019-01-19 16:00:00 +0000 UTC", s.CSTTime.UTC().String()) + assert.Equal(t, "2019-01-20 00:00:00 +0000 UTC", s.UTCTime.String()) + + // wrong location + var wrongLoc struct { + Time time.Time `time_location:"wrong"` + } + err = mapForm(&wrongLoc, map[string][]string{"Time": {"2019-01-20T16:02:58Z"}}) + assert.Error(t, err) + + // wrong time value + var wrongTime struct { + Time time.Time + } + err = mapForm(&wrongTime, map[string][]string{"Time": {"wrong"}}) + assert.Error(t, err) +} + +func TestMapiingTimeDuration(t *testing.T) { + var s struct { + D time.Duration + } + + // ok + err := mappingByPtr(&s, formSource{"D": {"5s"}}, "form") + assert.NoError(t, err) + assert.Equal(t, 5*time.Second, s.D) + + // error + err = mappingByPtr(&s, formSource{"D": {"wrong"}}, "form") + assert.Error(t, err) +} + +func TestMappingSlice(t *testing.T) { + var s struct { + Slice []int `form:"slice,default=9"` + } + + // default value + err := mappingByPtr(&s, formSource{}, "form") + assert.NoError(t, err) + assert.Equal(t, []int{9}, s.Slice) + + // ok + err = mappingByPtr(&s, formSource{"slice": {"3", "4"}}, "form") + assert.NoError(t, err) + assert.Equal(t, []int{3, 4}, s.Slice) + + // error + err = mappingByPtr(&s, formSource{"slice": {"wrong"}}, "form") + assert.Error(t, err) +} + +func TestMappingArray(t *testing.T) { + var s struct { + Array [2]int `form:"array,default=9"` + } + + // wrong default + err := mappingByPtr(&s, formSource{}, "form") + assert.Error(t, err) + + // ok + err = mappingByPtr(&s, formSource{"array": {"3", "4"}}, "form") + assert.NoError(t, err) + assert.Equal(t, [2]int{3, 4}, s.Array) + + // error - not enough vals + err = mappingByPtr(&s, formSource{"array": {"3"}}, "form") + assert.Error(t, err) + + // error - wrong value + err = mappingByPtr(&s, formSource{"array": {"wrong"}}, "form") + assert.Error(t, err) +} + +func TestMappingStructField(t *testing.T) { + var s struct { + J struct { + I int + } + } + + err := mappingByPtr(&s, formSource{"J": {`{"I": 9}`}}, "form") + assert.NoError(t, err) + assert.Equal(t, 9, s.J.I) +} + +func TestMappingMapField(t *testing.T) { + var s struct { + M map[string]int + } + + err := mappingByPtr(&s, formSource{"M": {`{"one": 1}`}}, "form") + assert.NoError(t, err) + assert.Equal(t, map[string]int{"one": 1}, s.M) +}