From 1fa855aaee799d1214b955ec9bed300d96292429 Mon Sep 17 00:00:00 2001 From: Anton Kholomiov Date: Mon, 20 Nov 2023 23:16:34 +0300 Subject: [PATCH 1/3] Reverse Html example --- .../HtmlTemplate/resources/haskell-logo.png | Bin 0 -> 23463 bytes .../HtmlTemplate/resources/lambda-logo.png | Bin 0 -> 2309 bytes .../HtmlTemplate/resources/milligram.min.css | 3 + .../mig-example-apps/HtmlTemplate/src/Api.hs | 97 +++++++ .../HtmlTemplate/src/Content.hs | 244 ++++++++++++++++++ .../mig-example-apps/HtmlTemplate/src/Init.hs | 79 ++++++ .../HtmlTemplate/src/Interface.hs | 21 ++ .../HtmlTemplate/src/Internal/State.hs | 43 +++ .../mig-example-apps/HtmlTemplate/src/Main.hs | 30 +++ .../HtmlTemplate/src/Server.hs | 117 +++++++++ .../HtmlTemplate/src/Types.hs | 61 +++++ .../mig-example-apps/HtmlTemplate/src/View.hs | 103 ++++++++ examples/mig-example-apps/Makefile | 5 +- .../mig-example-apps/mig-example-apps.cabal | 61 +++++ examples/mig-example-apps/package.yaml | 16 ++ mig-client/src/Mig/Client.hs | 5 - mig-extra/src/Mig/Extra/Server/Common.hs | 7 + mig-extra/src/Mig/Extra/Server/Html.hs | 2 +- mig-extra/src/Mig/Extra/Server/Json.hs | 4 +- mig-server/src/Mig.hs | 7 + mig/mig.cabal | 2 + mig/src/Mig/Core/Class.hs | 1 + mig/src/Mig/Core/Class/Url.hs | 139 ++++++++++ mig/src/Mig/Core/Types.hs | 1 + mig/src/Mig/Core/Types/Pair.hs | 8 + 25 files changed, 1047 insertions(+), 9 deletions(-) create mode 100644 examples/mig-example-apps/HtmlTemplate/resources/haskell-logo.png create mode 100644 examples/mig-example-apps/HtmlTemplate/resources/lambda-logo.png create mode 100644 examples/mig-example-apps/HtmlTemplate/resources/milligram.min.css create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Api.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Content.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Init.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Interface.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Internal/State.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Main.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Server.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/Types.hs create mode 100644 examples/mig-example-apps/HtmlTemplate/src/View.hs create mode 100644 mig/src/Mig/Core/Class/Url.hs create mode 100644 mig/src/Mig/Core/Types/Pair.hs diff --git a/examples/mig-example-apps/HtmlTemplate/resources/haskell-logo.png b/examples/mig-example-apps/HtmlTemplate/resources/haskell-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..d071f5c98b953e8b8ab666e3e1687a52ba8539bd GIT binary patch literal 23463 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 literal 0 HcmV?d00001 diff --git a/examples/mig-example-apps/HtmlTemplate/resources/lambda-logo.png b/examples/mig-example-apps/HtmlTemplate/resources/lambda-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..757fe67dab28f063dfdfa21bb58ca1659c24e08e GIT binary patch literal 2309 zcmV+g3HtVlP)w(X|Ni^=^mPFB*1q}P((`^3_M~w9_3-nFIsNhK^M4=iD&b#%umGzHE!Qo@l000OuNklZ`0}sg{44KHlYMp=F2d@M>}$Br_K6q&t+(5wC<}Z{2|)-=%MYAc`9DIb>j$gk z`7~^AH}o+=7{67pt`yMa4_tok`s+U<`-ueNup8KVzlT8qb^(*1x2KEd1aR>0(1!$QB1T#N2?nDX*bFr8Z5N&k z7`VGT7?c{Z0-g%_J)d@R1Qu{_xUr#8({;aRGogy8%2AW|I)0TfNeCjqf%H=7;tdNK zB&AS>mRo~mS@hdvrbs9?_IpS(_Q7+N8aYE$1!sYah8WEl38g}Fb1|&BB49Te+$gj_ zj5gQaiF6CCOc^BHm@wK5q!jSPNDTIh`v+_pTBh^K-$$~6oPwbk2KJ2fd;&OT5-?Vs z6fiPdl(*npL5u>{Xw0{Pt8h1kH*_#t)RwCx%1siOmDmQJbV(>+*GvrdNvOjw{cuMJ zVZm+48Ip!c-bkK|C=B9>5Q9Fc-f+T51UF}WKA*=N!fPxIz0HozigBVH9B{rv@ToI` zIEuY8RB?OGr#N{c&Upm8KXc{+AiWKnwe1$p_l$vj}#V( zRH;@Z0x9*(I0$VxsuhyYjXzZeY#)WQG1LZ+FV0e{@S5*~`XE*aqcHRhVvDJAYKKK> zj5YENdK*Q;D4<+9jCoRg)J#}d!Z+wGy@Jh`=c|^tXKN$c$S#b+)I}b+U8)j#Hp(C) zdKbRIP#d^wP#p100%s*N-(Y{S1&-yOA!KQu z+HNiY2aQr2jPg=u@pvhxykq-IwMrhM)0iF;x$>;o7=2+RuU9dQv>{=%X(1iM z92g$!Hh1enDU_V21{A`jexGM7H_}9sft@!JIhJZGk@~mkseJV8c5K7C2prKmgEE;m z(1v*SdBt4r_&ykP`CZV;X3ez0VK5o2#Iz}dO3MxNhR48e3&A{KJ_dzgfI;5ykh;5L zv{%3!(4kk}kTPA9Z(0`IpbXU!UpmI&7Mv-xz9Crp7c4XBkQqf6&_Ft*rU_O0oSO&C zGJU*4;YC{iTYN(cQA1IQA#_cMp4fV< zZe_5$kJP|7D71keQjpXjN^JO1ChPqEU1$QVmVh$_k{d-}bVy10fo|Nj8Yigx1O`z< zQKU~X=5wox96}j7OF|%RwrT-kYl$ypl|^2f(Uc1Jd|bzB>|@I(+yKQRd4U*0)punf zTTYjcj>KuYxt)c}znW}4050<>GkDcO<^rzrq^-7JK=B(HMH~Q^H31mj2HM|TzH80V zYa_4pK)rA2^cI8;0KzDUIuF&Ap;La7V>QI!1f4#CA+Mcn_((>9mwE6)n?8#|#JMhp zDM%AL%mtpb+Z0B;0<15d45p}iFc1Rist?#-U6pc1f#37!E1%E@V+)R!`UD2x8(I{$ zT#b-P9bn6|Qv&rq*QLr%*XiO)L6lw%>*nHRmbj&j+Id0~e>xQMN&#GPn3cxfMeG$9 z%?24_ToVH@$}PxgK8z^XnYfae->f2{7Ke0}h=@V%^5U7?l7c60J#{5;j*)nVd4TCJ z4!LJ>#GqeMcttvJL}W4W2@K+9MpuhAfRHIx);t>kMU;+MaVLW^`u4at55e`5^%FUi zdL=gX*sJ-kK!Ag(4s`%9z!Q^_7<^U~Qm}zr??#-#5^didwTIy8JDbGPPdJBJg%$#~ zlDwfsPa#&=!VW>hQh6QW0?x4f-LpzIT3Nthqu{P0Tj+y1`gS(G&tan~THINW^J=}8 zh-St{)#wu?=ervYYkN@&!e5yz#!0hD?Kw33K{s%i*U~Xe#^0evv7PC9S?lw|^zfs8 zWT(-2i-|kic~Ta(x5;t6O;lC$HiNx-Yjlz#nO$w#Cl5sYD@^5y$=rDGx-|JLF z1cmCS3Z>JUucyxw06?7V*12DXPD;-g3S&wUE}fO6UH?h2V=syJtNq39Hv8x={&4Gs f7hZVb|AK!27O{UUUZ1=)00000NkvXXu0mjfprmHg literal 0 HcmV?d00001 diff --git a/examples/mig-example-apps/HtmlTemplate/resources/milligram.min.css b/examples/mig-example-apps/HtmlTemplate/resources/milligram.min.css new file mode 100644 index 0000000..5e8955c --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/resources/milligram.min.css @@ -0,0 +1,3 @@ +*,*:after,*:before{box-sizing:inherit}html{box-sizing:border-box;font-size:62.5%}body{color:#606c76;font-family:'Roboto', 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif;font-size:1.6em;font-weight:300;letter-spacing:.01em;line-height:1.6}blockquote{border-left:0.3rem solid #d1d1d1;margin-left:0;margin-right:0;padding:1rem 1.5rem}blockquote *:last-child{margin-bottom:0}.button,button,input[type='button'],input[type='reset'],input[type='submit']{background-color:#9b4dca;border:0.1rem solid #9b4dca;border-radius:.4rem;color:#fff;cursor:pointer;display:inline-block;font-size:1.1rem;font-weight:700;height:3.8rem;letter-spacing:.1rem;line-height:3.8rem;padding:0 3.0rem;text-align:center;text-decoration:none;text-transform:uppercase;white-space:nowrap}.button:focus,.button:hover,button:focus,button:hover,input[type='button']:focus,input[type='button']:hover,input[type='reset']:focus,input[type='reset']:hover,input[type='submit']:focus,input[type='submit']:hover{background-color:#606c76;border-color:#606c76;color:#fff;outline:0}.button[disabled],button[disabled],input[type='button'][disabled],input[type='reset'][disabled],input[type='submit'][disabled]{cursor:default;opacity:.5}.button[disabled]:focus,.button[disabled]:hover,button[disabled]:focus,button[disabled]:hover,input[type='button'][disabled]:focus,input[type='button'][disabled]:hover,input[type='reset'][disabled]:focus,input[type='reset'][disabled]:hover,input[type='submit'][disabled]:focus,input[type='submit'][disabled]:hover{background-color:#9b4dca;border-color:#9b4dca}.button.button-outline,button.button-outline,input[type='button'].button-outline,input[type='reset'].button-outline,input[type='submit'].button-outline{background-color:transparent;color:#9b4dca}.button.button-outline:focus,.button.button-outline:hover,button.button-outline:focus,button.button-outline:hover,input[type='button'].button-outline:focus,input[type='button'].button-outline:hover,input[type='reset'].button-outline:focus,input[type='reset'].button-outline:hover,input[type='submit'].button-outline:focus,input[type='submit'].button-outline:hover{background-color:transparent;border-color:#606c76;color:#606c76}.button.button-outline[disabled]:focus,.button.button-outline[disabled]:hover,button.button-outline[disabled]:focus,button.button-outline[disabled]:hover,input[type='button'].button-outline[disabled]:focus,input[type='button'].button-outline[disabled]:hover,input[type='reset'].button-outline[disabled]:focus,input[type='reset'].button-outline[disabled]:hover,input[type='submit'].button-outline[disabled]:focus,input[type='submit'].button-outline[disabled]:hover{border-color:inherit;color:#9b4dca}.button.button-clear,button.button-clear,input[type='button'].button-clear,input[type='reset'].button-clear,input[type='submit'].button-clear{background-color:transparent;border-color:transparent;color:#9b4dca}.button.button-clear:focus,.button.button-clear:hover,button.button-clear:focus,button.button-clear:hover,input[type='button'].button-clear:focus,input[type='button'].button-clear:hover,input[type='reset'].button-clear:focus,input[type='reset'].button-clear:hover,input[type='submit'].button-clear:focus,input[type='submit'].button-clear:hover{background-color:transparent;border-color:transparent;color:#606c76}.button.button-clear[disabled]:focus,.button.button-clear[disabled]:hover,button.button-clear[disabled]:focus,button.button-clear[disabled]:hover,input[type='button'].button-clear[disabled]:focus,input[type='button'].button-clear[disabled]:hover,input[type='reset'].button-clear[disabled]:focus,input[type='reset'].button-clear[disabled]:hover,input[type='submit'].button-clear[disabled]:focus,input[type='submit'].button-clear[disabled]:hover{color:#9b4dca}code{background:#f4f5f6;border-radius:.4rem;font-size:86%;margin:0 .2rem;padding:.2rem .5rem;white-space:nowrap}pre{background:#f4f5f6;border-left:0.3rem solid #9b4dca;overflow-y:hidden}pre>code{border-radius:0;display:block;padding:1rem 1.5rem;white-space:pre}hr{border:0;border-top:0.1rem solid #f4f5f6;margin:3.0rem 0}input[type='color'],input[type='date'],input[type='datetime'],input[type='datetime-local'],input[type='email'],input[type='month'],input[type='number'],input[type='password'],input[type='search'],input[type='tel'],input[type='text'],input[type='url'],input[type='week'],input:not([type]),textarea,select{-webkit-appearance:none;background-color:transparent;border:0.1rem solid #d1d1d1;border-radius:.4rem;box-shadow:none;box-sizing:inherit;height:3.8rem;padding:.6rem 1.0rem .7rem;width:100%}input[type='color']:focus,input[type='date']:focus,input[type='datetime']:focus,input[type='datetime-local']:focus,input[type='email']:focus,input[type='month']:focus,input[type='number']:focus,input[type='password']:focus,input[type='search']:focus,input[type='tel']:focus,input[type='text']:focus,input[type='url']:focus,input[type='week']:focus,input:not([type]):focus,textarea:focus,select:focus{border-color:#9b4dca;outline:0}select{background:url('data:image/svg+xml;utf8,') center right no-repeat;padding-right:3.0rem}select:focus{background-image:url('data:image/svg+xml;utf8,')}select[multiple]{background:none;height:auto}textarea{min-height:6.5rem}label,legend{display:block;font-size:1.6rem;font-weight:700;margin-bottom:.5rem}fieldset{border-width:0;padding:0}input[type='checkbox'],input[type='radio']{display:inline}.label-inline{display:inline-block;font-weight:normal;margin-left:.5rem}.container{margin:0 auto;max-width:112.0rem;padding:0 2.0rem;position:relative;width:100%}.row{display:flex;flex-direction:column;padding:0;width:100%}.row.row-no-padding{padding:0}.row.row-no-padding>.column{padding:0}.row.row-wrap{flex-wrap:wrap}.row.row-top{align-items:flex-start}.row.row-bottom{align-items:flex-end}.row.row-center{align-items:center}.row.row-stretch{align-items:stretch}.row.row-baseline{align-items:baseline}.row .column{display:block;flex:1 1 auto;margin-left:0;max-width:100%;width:100%}.row .column.column-offset-10{margin-left:10%}.row .column.column-offset-20{margin-left:20%}.row .column.column-offset-25{margin-left:25%}.row .column.column-offset-33,.row .column.column-offset-34{margin-left:33.3333%}.row .column.column-offset-40{margin-left:40%}.row .column.column-offset-50{margin-left:50%}.row .column.column-offset-60{margin-left:60%}.row .column.column-offset-66,.row .column.column-offset-67{margin-left:66.6666%}.row .column.column-offset-75{margin-left:75%}.row .column.column-offset-80{margin-left:80%}.row .column.column-offset-90{margin-left:90%}.row .column.column-10{flex:0 0 10%;max-width:10%}.row .column.column-20{flex:0 0 20%;max-width:20%}.row .column.column-25{flex:0 0 25%;max-width:25%}.row .column.column-33,.row .column.column-34{flex:0 0 33.3333%;max-width:33.3333%}.row .column.column-40{flex:0 0 40%;max-width:40%}.row .column.column-50{flex:0 0 50%;max-width:50%}.row .column.column-60{flex:0 0 60%;max-width:60%}.row .column.column-66,.row .column.column-67{flex:0 0 66.6666%;max-width:66.6666%}.row .column.column-75{flex:0 0 75%;max-width:75%}.row .column.column-80{flex:0 0 80%;max-width:80%}.row .column.column-90{flex:0 0 90%;max-width:90%}.row .column .column-top{align-self:flex-start}.row .column .column-bottom{align-self:flex-end}.row .column .column-center{align-self:center}@media (min-width: 40rem){.row{flex-direction:row;margin-left:-1.0rem;width:calc(100% + 2.0rem)}.row .column{margin-bottom:inherit;padding:0 1.0rem}}a{color:#9b4dca;text-decoration:none}a:focus,a:hover{color:#606c76}dl,ol,ul{list-style:none;margin-top:0;padding-left:0}dl dl,dl ol,dl ul,ol dl,ol ol,ol ul,ul dl,ul ol,ul ul{font-size:90%;margin:1.5rem 0 1.5rem 3.0rem}ol{list-style:decimal inside}ul{list-style:circle inside}.button,button,dd,dt,li{margin-bottom:1.0rem}fieldset,input,select,textarea{margin-bottom:1.5rem}blockquote,dl,figure,form,ol,p,pre,table,ul{margin-bottom:2.5rem}table{border-spacing:0;display:block;overflow-x:auto;text-align:left;width:100%}td,th{border-bottom:0.1rem solid #e1e1e1;padding:1.2rem 1.5rem}td:first-child,th:first-child{padding-left:0}td:last-child,th:last-child{padding-right:0}@media (min-width: 40rem){table{display:table;overflow-x:initial}}b,strong{font-weight:bold}p{margin-top:0}h1,h2,h3,h4,h5,h6{font-weight:300;letter-spacing:-.1rem;margin-bottom:2.0rem;margin-top:0}h1{font-size:4.6rem;line-height:1.2}h2{font-size:3.6rem;line-height:1.25}h3{font-size:2.8rem;line-height:1.3}h4{font-size:2.2rem;letter-spacing:-.08rem;line-height:1.35}h5{font-size:1.8rem;letter-spacing:-.05rem;line-height:1.5}h6{font-size:1.6rem;letter-spacing:0;line-height:1.4}img{max-width:100%}.clearfix:after{clear:both;content:' ';display:table}.float-left{float:left}.float-right{float:right} + +/*# sourceMappingURL=milligram.min.css.map */ \ No newline at end of file diff --git a/examples/mig-example-apps/HtmlTemplate/src/Api.hs b/examples/mig-example-apps/HtmlTemplate/src/Api.hs new file mode 100644 index 0000000..e7a2711 --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Api.hs @@ -0,0 +1,97 @@ +module Api ( + Routes (..), + Urls (..), + GreetingRoute, + BlogPostRoute, + QuoteRoute, + WriteFormRoute, + WriteSubmitRoute, + ListPostsRoute, + urls, + server, +) where + +import Mig.Html.IO +import Types + +-- routes + +type GreetingRoute = Get Html +type BlogPostRoute = Optional "id" BlogPostId -> Get Html +type QuoteRoute = Get Html +type WriteFormRoute = Get Html +type WriteSubmitRoute = Body FormUrlEncoded SubmitBlogPost -> Post Html +type ListPostsRoute = Get Html + +data Routes = Routes + { greeting :: GreetingRoute + , blogPost :: BlogPostRoute + , quote :: QuoteRoute + , listPosts :: ListPostsRoute + , writeForm :: WriteFormRoute + , writeSubmit :: WriteSubmitRoute + } + +-- URLs + +data Urls = Urls + { greeting :: UrlOf GreetingRoute + , blogPost :: UrlOf BlogPostRoute + , quote :: UrlOf QuoteRoute + , listPosts :: UrlOf ListPostsRoute + , writeForm :: UrlOf WriteFormRoute + , writeSubmit :: UrlOf WriteSubmitRoute + } + +{-| Site URL's +URL's should be listed in the same order as they appear in the server +-} +urls :: Urls +urls = Urls{..} + where + greeting + :| blogPost + :| quote + :| listPosts + :| writeForm + :| writeSubmit = + toUrl (server undefined) + +-- server definition + +-- | Server definition. Note how we assemble it from parts with monoid method mconcat. +server :: Routes -> Server IO +server routes = + addIndex $ + mconcat + [ defaultPage + , "blog" + /. [ readServer + , writeServer + ] + ] + where + addIndex = addPathLink "index.html" "/" + + -- default main page + defaultPage = + "/" /. routes.greeting + + -- server to read info. + -- We can read blog posts and quotes. + readServer = + toServer + [ "read" + /. mconcat + [ "post" /. routes.blogPost + , "quote" /. routes.quote + ] + , "list" /. routes.listPosts + ] + + -- server to write new blog posts + writeServer = + "write" + /. [ toServer routes.writeForm + , toServer routes.writeSubmit + ] diff --git a/examples/mig-example-apps/HtmlTemplate/src/Content.hs b/examples/mig-example-apps/HtmlTemplate/src/Content.hs new file mode 100644 index 0000000..1f9527e --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Content.hs @@ -0,0 +1,244 @@ +-- | random content for blog posts +module Content ( + poems, + quotes, +) where + +import Data.Text (Text) +import Data.Text qualified as Text + +poems :: [Text] +poems = [lukomorye, tweedle, peasants, dream, pie, zhongnanMountains, zvyozdy, toSeeAWorld] + +lukomorye :: Text +lukomorye = + Text.unlines + [ "У лукоморья дуб зелёный" + , "Златая цепь на дубе том" + , "И днём и ночью кот учёный" + , "Всё ходит по цепи кругом;" + , "Идёт направо — песнь заводит," + , "Налево — сказку говорит." + , "Там чудеса: там леший бродит," + , "Русалка на ветвях сидит;" + , "Там на неведомых дорожках" + , "Следы невиданных зверей;" + , "Избушка там на курьих ножках" + , "Стоит без окон, без дверей;" + , "Там лес и дол видений полны;" + , "Там о заре прихлынут волны" + , "На брег песчаный и пустой," + , "И тридцать витязей прекрасных" + , "Чредой из вод выходят ясных," + , "И с ними дядька их морской;" + , "Там королевич мимоходом" + , "Пленяет грозного царя;" + , "Там в облаках перед народом" + , "Через леса, через моря" + , "Колдун несёт богатыря;" + , "В темнице там царевна тужит," + , "А бурый волк ей верно служит;" + , "Там ступа с Бабою Ягой" + , "Идёт, бредёт сама собой," + , "Там царь Кащей над златом чахнет;" + , "Там русский дух… там Русью пахнет!" + , "И там я был, и мёд я пил;" + , "У моря видел дуб зелёный;" + , "Под ним сидел, и кот учёный" + , "Свои мне сказки говорил." + , "" + , "Александр Пушкин" + ] + +dream :: Text +dream = + Text.unlines + [ "И это снилось мне, и это снится мне," + , "И это мне ещё когда-нибудь приснится," + , "И повторится всё, и всё довоплотится," + , "И вам приснится всё, что видел я во сне." + , "" + , "Там, в стороне от нас, от мира в стороне" + , "Волна идёт вослед волне о берег биться," + , "А на волне звезда, и человек, и птица," + , "И явь, и сны, и смерть — волна вослед волне." + , "" + , "Не надо мне числа: я был, и есмь, и буду," + , "Жизнь — чудо из чудес, и на колени чуду" + , "Один, как сирота, я сам себя кладу," + , "Один, среди зеркал — в ограде отражений" + , "Морей и городов, лучащихся в чаду." + , "И мать в слезах берёт ребёнка на колени." + , "" + , "Арсений Тарковский" + ] + +pie :: Text +pie = + Text.unlines + [ "Cottleston Cottleston Cottleston Pie" + , "A fly can't bird, but a bird can fly." + , "Ask me a riddle and I reply" + , "Cottleston Cottleston Cottleston Pie." + , "" + , "Cottleston Cottleston Cottleston Pie," + , "Why does a chicken? I don't know why." + , "Ask me a riddle and I reply" + , "Cottleston Cottleston Cottleston Pie." + , "" + , "Alan Milne" + ] + +peasants :: Text +peasants = + Text.unlines + [ "锄禾日当午," + , "汗滴禾下土。" + , "谁知盘中餐," + , "粒粒皆辛苦。" + , "" + , "李绅" + ] + +toSeeAWorld :: Text +toSeeAWorld = + Text.unlines + [ "To see a World in a Grain of Sand" + , "And a Heaven in a Wild Flower," + , "Hold Infinity in the palm of your hand" + , "And Eternity in an hour" + , "" + , "William Blake" + ] + +zhongnanMountains :: Text +zhongnanMountains = + Text.unlines + [ "太乙近天都," + , "连山到海隅。" + , "白云回望合," + , "青霭入看无。" + , "分野中峰变," + , "阴晴众壑殊。" + , "欲投人处宿," + , "隔水问樵夫。" + , "" + , "王维" + ] + +tweedle :: Text +tweedle = + Text.unlines + [ "Tweedledum and Tweedledee" + , "Agreed to have a battle;" + , "For Tweedledum said Tweedledee" + , "Had spoiled his nice new rattle." + , "Just then flew down a monstrous crow," + , "As black as a tar-barrel;" + , "Which frightened both the heroes so," + , "They quite forgot their quarrel." + , "" + , "Lewis Carroll" + ] + +zvyozdy :: Text +zvyozdy = + Text.unlines + [ "По ночам, когда в тумане" + , "Звезды в небе время ткут," + , "Я ловлю разрывы ткани" + , "В вечном кружеве минут." + , "Я ловлю в мгновенья эти," + , "Как свивается покров" + , "Со всего, что в формах, в цвете," + , "Со всего, что в звуке слов." + , "Да, я помню мир иной —" + , "Полустертый, непохожий," + , "В вашем мире я — прохожий," + , "Близкий всем, всему чужой." + , "Ряд случайных сочетаний" + , "Мировых путей и сил" + , "В этот мир замкнутых граней" + , "Влил меня и воплотил." + , "Как ядро к ноге прикован" + , "Шар земной. Свершая путь," + , "Я не смею, зачарован," + , "Вниз на звезды заглянуть." + , "Что одни зовут звериным," + , "Что одни зовут людским —" + , "Мне, который был единым," + , "Стать отдельным и мужским!" + , "Вечность с жгучей пустотою" + , "Неразгаданных чудес" + , "Скрыта близкой синевою" + , "Примиряющих небес." + , "Мне так радостно и ново" + , "Все обычное для вас —" + , "Я люблю обманность слова" + , "И прозрачность ваших глаз." + , "Ваши детские понятья" + , "Смерти, зла, любви, грехов —" + , "Мир души, одетый в платье" + , "Из священных, лживых слов." + , "Гармонично и поблёкло" + , "В них мерцает мир вещей," + , "Как узорчатые стекла" + , "В мгле готических церквей…" + , "В вечных поисках истоков" + , "Я люблю в себе следить" + , "Жутких мыслей и пороков" + , "Нас связующую нить." + , "Когда ж уйду я в вечность снова?" + , "И мне раскроется она," + , "Так ослепительно ясна" + , "Так беспощадна, так сурова" + , "И звездным ужасом полна!" + , "" + , "Максимилиан Волошин" + ] + +------------------------------------------------------------------------------------- +-- random content for quotes + +quotes :: [Text] +quotes = + [ "“We ascribe beauty to that which is simple; which has no superfluous parts; which exactly answers its end; which stands related to all things; which is the mean of many extremes.” - Ralph Waldo Emerson" + , "“Each day a few more lies eat into the seed with which we are born, little institutional lies from the print of newspapers, the shock waves of television, and the sentimental cheats of the movie screen.” - Norman Mailer" + , "“Let us so live that when we come to die even the undertaker will be sorry.” - Mark Twain" + , "“When we seek to discover the best in others, we somehow bring out the best in ourselves.” - William Arthur Ward" + , "“Make things as simple as possible, but not simpler.” - Albert Einstein" + , "“Go often to the house of thy friend, for weeds choke the unused path.” - Ralph Waldo Emerson" + , "“Hating people is like burning down your own house to get rid of a rat.” - Henry Emerson Fosdick" + , "“The most pitiful among men is he who turns his dreams into silver and gold.” - Kahlil Gibran" + , "“Persistent people begin their success where others end in failures.” - Edward Eggleston" + , "“Pick battles big enough to matter, small enough to win.” - Jonathan Kozol" + , "“Show me a man with both feet on the ground, and I’ll show you a man who can’t put his pants on.” - Arthur K. Watson" + , "“The more original a discovery, the more obvious it seems afterward.” - Arthur Koestler" + , "“Loyalty to a petrified opinion never yet broke a chain or freed a human soul.” - Mark Twain" + , "“O senseless man, who cannot possibly make a worm and yet will make Gods by the dozen!” - Michel de Montaigne" + , "“Talent develops in tranquility, character in the full current of human life.” - Johann Wolfgang von Goethe" + , "“The happiness of your life depends upon the quality of your thoughts: therefore, guard accordingly, and take care that you entertain no notions unsuitable to virtue and reasonable nature.” - Marcus Aurelius" + , "“Character cannot be developed in ease and quiet. Only through experience of trial and suffering can the soul be strengthened, ambition inspired, and success achieved.” - Helen Adams Keller" + , "“There is nothing with which every man is so afraid as getting to know how enormously much he is capable of doing and becoming.” - Soren Kierkegaard" + , "“If you want to build a ship, don’t drum up people together to collect wood and don’t assign them tasks and work, but rather teach them to long for the endless immensity of the sea.” - Antoine de Saint-Exupéry" + , "“Inside every large program is a small program struggling to get out.” - Tony Hoare" + , "“Man is most nearly himself when he achieves the seriousness of a child at play.” - Heraclitus" + , "“Most of us are just about as happy as we make up our minds to be.” - Abraham Lincoln" + , "“Keep away from people who try to belittle your ambitions. Small people always do that, but the really great make you feel that you, too, can become great.” - Mark Twain" + , "“The best and most beautiful things in the world cannot be seen or even touched. They must be felt with the heart.” - Helen Adams Keller" + , "“If words are to enter men’s minds and bear fruit, they must be the right words shaped cunningly to pass men’s defenses and explode silently and effectually within their minds.” - J.B. Phillips" + , "“A cynic is not merely one who reads bitter lessons from the past; he is one who is prematurely disappointed in the future.” - Sydney J. Harris" + , "Do not praise yourself\nnot slander others:\nThere are still many days to go\nand any thing could happen.\n\nKabir" + , "“War is an instrument entirely inefficient toward redressing wrong; and multiplies, instead of indemnifying losses.” - Thomas Jefferson" + , "“It is preoccupation with possessions, more than anything else, that prevents us from living freely and nobly.” - Bertrand Russell" + , "“New and stirring things are belittled because if they are not belittled, the humiliating question arises, ``Why then are you not taking part in them?’’” - H.G. Wells" + , "“The mass of men lead lives of quiet desperation and go to the grave with the song still in them.” - Henry David Thoreau" + , "“Noise proves nothing—often a hen who has merely laid an egg cackles as if she had laid an asteroid.” - Mark Twain" + , "“It is easier to fight for one’s principles than to live up to them.” - Alfred Adler" + , "“We could never learn to be brave and patient, if there were only joy in the world.” - Helen Adams Keller" + , "“One is always a long way from solving a problem until one actually has the answer.” - Stephen Hawking" + , "“Knowing all truth is less than doing a little bit of good.” - Albert Schweitzer" + , "“If a man points at the moon, an idiot will look at the finger.” - Sufi wisdom" + , "“A friend is a person with whom I may be sincere. Before him I may think aloud.” - Ralph Waldo Emerson" + , "“Happiness is the absence of the striving for happiness.” - Chuang-tzu" + ] diff --git a/examples/mig-example-apps/HtmlTemplate/src/Init.hs b/examples/mig-example-apps/HtmlTemplate/src/Init.hs new file mode 100644 index 0000000..391feb8 --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Init.hs @@ -0,0 +1,79 @@ +module Init ( + initSite, +) where + +import Data.IORef +import Data.List qualified as List +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Time +import System.Log.FastLogger +import System.Random + +import Content +import Interface +import Internal.State +import Types + +{-| Initialise the logic for our website. +we read the posts from some poems and fill the site with them. + +Also we init all actions. Note how we hide the mutable state Env with interface for Site. +-} +initSite :: IO Site +initSite = do + env <- initEnv + (writeLog, closeLogger) <- newFastLogger (LogStdout defaultBufSize) + let logInfo msg = do + now <- getCurrentTime + writeLog $ + toLogStr $ + Text.unwords + [ "[INFO]:" + , msg <> "." + , "at" + , Text.pack (show now) <> "\n" + ] + pure $ + Site + { readBlogPost = mockRead env + , writeBlogPost = mockWriteBlogPost env + , listBlogPosts = readIORef env.blogPosts + , readQuote = Quote <$> randomQuote + , logInfo = logInfo + , cleanup = do + logInfo "Blog site shutdown" + closeLogger + } + +------------------------------------------------------------------------------------- +-- implementation of the site interfaces. +-- It defines how to read blog posts and quotes and how to create new posts +-- note how we use IO-actions. We can also read from DB or do all sorts of things here. + +-- | Read the blog post +mockRead :: Env -> BlogPostId -> IO (Maybe BlogPost) +mockRead env postId = do + blogPosts <- readIORef env.blogPosts + pure (List.find (\post -> post.id == postId) blogPosts) + +-- | Write new blog post +mockWriteBlogPost :: Env -> SubmitBlogPost -> IO BlogPostId +mockWriteBlogPost env (SubmitBlogPost title content) = do + pid <- randomBlogPostId + time <- getCurrentTime + -- unsafe in concurrent, it is here just for example (use TVar or atomicModifyIORef) + modifyIORef' env.blogPosts (BlogPost pid title time content :) + pure pid + +randomQuote :: IO Text +randomQuote = oneOf quotes + +------------------------------------------------------------------------------------- +-- utils + +-- pick random element from a list +oneOf :: [a] -> IO a +oneOf as = (as !!) . (`mod` len) <$> randomIO + where + len = length as diff --git a/examples/mig-example-apps/HtmlTemplate/src/Interface.hs b/examples/mig-example-apps/HtmlTemplate/src/Interface.hs new file mode 100644 index 0000000..8a5abf7 --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Interface.hs @@ -0,0 +1,21 @@ +{-| Site interfaces as abstractions over interaction with outside world. +For example logging, storing new posts in DB, etc. +-} +module Interface ( + Site (..), +) where + +import Data.Text (Text) +import Types + +{-| Web site actions. It defines interfaces that connect logic of our site +with outside world: DBs, logger. +-} +data Site = Site + { readBlogPost :: BlogPostId -> IO (Maybe BlogPost) + , writeBlogPost :: SubmitBlogPost -> IO BlogPostId + , listBlogPosts :: IO [BlogPost] + , readQuote :: IO Quote + , logInfo :: Text -> IO () + , cleanup :: IO () + } diff --git a/examples/mig-example-apps/HtmlTemplate/src/Internal/State.hs b/examples/mig-example-apps/HtmlTemplate/src/Internal/State.hs new file mode 100644 index 0000000..7391ca8 --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Internal/State.hs @@ -0,0 +1,43 @@ +module Internal.State ( + Env (..), + initEnv, + randomBlogPostId, +) where + +import Content +import Types + +import Data.IORef +import Data.Text (Text) +import Data.Text qualified as Text +import Data.Time +import System.Random + +-- | Site mutable state +data Env = Env + { blogPosts :: IORef [BlogPost] + -- ^ for example we store posts in memory but it also can become a DB. + } + +initEnv :: IO Env +initEnv = do + posts <- mapM poemToBlogPost poems + Env <$> newIORef posts + +poemToBlogPost :: Text -> IO BlogPost +poemToBlogPost poem = do + pid <- randomBlogPostId + time <- getCurrentTime + pure $ + BlogPost + { id = pid + , createdAt = time + , title = + let ls = Text.lines poem + in mconcat [last ls, ": ", head ls] + , content = poem + } + +-- | allocates fresh id for blog post +randomBlogPostId :: IO BlogPostId +randomBlogPostId = BlogPostId <$> randomIO diff --git a/examples/mig-example-apps/HtmlTemplate/src/Main.hs b/examples/mig-example-apps/HtmlTemplate/src/Main.hs new file mode 100644 index 0000000..0cc1613 --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Main.hs @@ -0,0 +1,30 @@ +{-| Example on how to serve Html + +We create a simple blog post site which offers two types of content: + +* blog posts +* quotes + +We can choose between the two. Also we can create new blog posts and list them. +all posts are stored in memory. +-} +module Main ( + main, +) where + +import Control.Exception (finally) +import Data.Text qualified as Text +import Init (initSite) +import Interface +import Mig.Html.IO (runServer) +import Server (initServer) + +-- run blog post server +main :: IO () +main = do + site <- initSite + site.logInfo ("The blog post server listens on port: " <> Text.pack (show port)) + runServer port (initServer site) + `finally` site.cleanup + where + port = 8085 diff --git a/examples/mig-example-apps/HtmlTemplate/src/Server.hs b/examples/mig-example-apps/HtmlTemplate/src/Server.hs new file mode 100644 index 0000000..2ba8112 --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Server.hs @@ -0,0 +1,117 @@ +{-# LANGUAGE TemplateHaskell #-} + +-- server +module Server ( + initServer, +) where + +import Control.Monad +import Data.ByteString (ByteString) +import Data.Text qualified as Text +import FileEmbedLzma + +import Mig.Html.IO +import System.Random + +import Api +import Interface +import Types +import View () + +-- server definition + +initServer :: Site -> Server IO +initServer site = logRoutes $ server (initRoutes site) <> staticServer + where + staticServer :: Server IO + staticServer = + addFavicon $ "static" /. staticFiles resourceFiles + + resourceFiles :: [(FilePath, ByteString)] + resourceFiles = $(embedRecursiveDir "Html/resources") + + addFavicon :: Server IO -> Server IO + addFavicon = addPathLink "favicon.ico" "static/lambda-logo.png" + + logRoutes :: Server IO -> Server IO + logRoutes = applyPlugin $ \(FullPathInfo path) -> prependServerAction $ + when (not $ path == "favicon.ico" || Text.isPrefixOf "static" path) $ do + logRoute site path + +------------------------------------------------------------------------------------- +-- server handlers + +initRoutes :: Site -> Routes +initRoutes site = + Routes + { greeting = handleGreeting site + , blogPost = handleBlogPost site + , quote = handleQuote site + , writeForm = handleWriteForm site + , writeSubmit = handleWriteSubmit site + , listPosts = handleListPosts site + } + +-- | Greet the user on main page +handleGreeting :: Site -> GreetingRoute +handleGreeting site = + Send $ toPage . Greeting <$> site.listBlogPosts + +-- | Read blog post by id +handleBlogPost :: Site -> BlogPostRoute +handleBlogPost site (Optional mBlogId) = Send $ + case mBlogId of + Nothing -> toPage . ViewBlogPost <$> randomBlogPost site + Just blogId -> + maybe + (toErrorPage notFound404 $ PostNotFound blogId) + (toPage . ViewBlogPost) + <$> site.readBlogPost blogId + +-- | Read random quote +handleQuote :: Site -> QuoteRoute +handleQuote site = Send $ toPage <$> site.readQuote + +-- | Show form to the user to fill new post data +handleWriteForm :: Site -> WriteFormRoute +handleWriteForm _site = + pure $ toPage WritePost + +-- | Submit form with data provided by the user +handleWriteSubmit :: Site -> WriteSubmitRoute +handleWriteSubmit site (Body submitData) = Send $ do + pid <- site.writeBlogPost submitData + maybe + (toErrorPage notFound404 $ PostNotFound pid) + (toPage . ViewBlogPost) + <$> site.readBlogPost pid + +-- | List all posts so far +handleListPosts :: Site -> ListPostsRoute +handleListPosts site = Send $ do + toPage . ListPosts <$> site.listBlogPosts + +-- | Logs the route info +logRoute :: Site -> Text -> IO () +logRoute site route = do + site.logInfo $ route <> " page visited" + +-- | Get random blog post +randomBlogPost :: Site -> IO BlogPost +randomBlogPost site = + oneOf =<< site.listBlogPosts + +toPage :: (ToMarkup a) => a -> Resp Html +toPage = ok . toMarkup . Page + +toErrorPage :: (ToMarkup a) => Status -> a -> Resp Html +toErrorPage status = bad status . toMarkup . Page + +------------------------------------------------------------------------------------- +-- utils + +-- pick random element from a list +oneOf :: [a] -> IO a +oneOf as = (as !!) . (`mod` len) <$> randomIO + where + len = length as diff --git a/examples/mig-example-apps/HtmlTemplate/src/Types.hs b/examples/mig-example-apps/HtmlTemplate/src/Types.hs new file mode 100644 index 0000000..caef6b7 --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/Types.hs @@ -0,0 +1,61 @@ +-- | types +module Types ( + Page (..), + Greeting (..), + WritePost (..), + ListPosts (..), + BlogPostId (..), + BlogPostView (..), + BlogPost (..), + Quote (..), + SubmitBlogPost (..), +) where + +import Data.Time +import Data.UUID +import Mig.Html.IO + +-- | Web-page for our site +newtype Page a = Page a + +-- | Greeting page +newtype Greeting = Greeting [BlogPost] + +-- | Form to submit new post +data WritePost = WritePost + +-- | List all posts +newtype ListPosts = ListPosts [BlogPost] + +-- | Blog post id +newtype BlogPostId = BlogPostId {unBlogPostId :: UUID} + +data BlogPostView + = ViewBlogPost BlogPost + | -- | error: post not found by id + PostNotFound BlogPostId + +-- | Blog post +data BlogPost = BlogPost + { id :: BlogPostId + , title :: Text + , createdAt :: UTCTime + , content :: Text + } + +-- | A quote +data Quote = Quote + { content :: Text + } + +-- | Data to submit new blog post +data SubmitBlogPost = SubmitBlogPost + { title :: Text + , content :: Text + } + +-------------------------------------------- +-- derivings + +mapDerive deriveNewtypeParam [''BlogPostId] +deriveForm ''SubmitBlogPost diff --git a/examples/mig-example-apps/HtmlTemplate/src/View.hs b/examples/mig-example-apps/HtmlTemplate/src/View.hs new file mode 100644 index 0000000..dbc4b2c --- /dev/null +++ b/examples/mig-example-apps/HtmlTemplate/src/View.hs @@ -0,0 +1,103 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} + +-- | html renderers. View for all pages +module View () where + +import Api (Urls (..), urls) +import Data.List qualified as List +import Mig +import Text.Blaze.Html5 qualified as H +import Text.Blaze.Html5.Attributes qualified as HA +import Types + +-- writes the template for main page +instance (ToMarkup a) => ToMarkup (Page a) where + toMarkup page = case page of + Page a -> siteTemplate (H.toMarkup a) + +-- | Main site template +siteTemplate :: Html -> Html +siteTemplate content = H.html $ do + H.head $ do + H.meta H.! HA.charset "UTF-8" + H.link H.! HA.rel "stylesheet" H.! HA.href "https://fonts.googleapis.com/css?family=Roboto:300,300italic,700,700italic" + H.link H.! HA.rel "stylesheet" H.! HA.href "/static/milligram.min.css" + H.body $ H.div H.! HA.style "margin-left:4%; margin-top: 3%; font-size: 110%" $ do + H.div H.! HA.class_ "container" $ do + H.div H.! HA.class_ "row" $ do + H.div H.! HA.class_ "column column-20" $ menu + H.div H.! HA.class_ "column column-75 column-offset-5" $ content + where + menu = do + H.div $ do + H.img H.! HA.src "/static/haskell-logo.png" H.! HA.alt "blog logo" H.! HA.width "100pt" H.! HA.style "margin-bottom: 15pt" + H.ul H.! HA.style "list-style: none" $ do + item (renderUrl urls.greeting) "main page" + item (renderUrl $ urls.blogPost $ Optional Nothing) "next post" + item (renderUrl urls.quote) "next quote" + item (renderUrl urls.writeForm) "write new post" + item (renderUrl urls.listPosts) "list all posts" + + item ref name = + H.li $ H.a H.! HA.href ref $ H.text name + +-- Rendering of the greeting page +instance ToMarkup Greeting where + toMarkup (Greeting posts) = do + H.div $ do + H.h2 "Welcome to blog site example" + H.p "You can get random poem or random quote from menu bar" + toMarkup (ListPosts posts) + +-- Rendering of the form to submit the post +instance ToMarkup WritePost where + toMarkup WritePost = do + H.div $ do + H.h2 "Write new post" + H.form H.! HA.method "POST" H.! HA.action "/blog/write" $ do + inputText "title" + inputContent "content" + submit "Save blog post" + where + inputText name = H.div $ do + H.p (H.text $ "Input " <> name) + H.textarea H.! HA.rows "1" H.! HA.cols "100" H.! HA.id (H.toValue name) H.! HA.name (H.toValue name) $ pure () + + inputContent name = H.div $ do + H.p (H.text $ "Input " <> name) + H.textarea H.! HA.rows "10" H.! HA.cols "100" H.! HA.id (H.toValue name) H.! HA.name (H.toValue name) $ pure () + + submit :: Text -> Html + submit name = H.div $ H.input H.! HA.type_ "submit" H.! HA.value (H.toValue name) + +instance ToMarkup BlogPostView where + toMarkup = \case + ViewBlogPost post -> toMarkup post + PostNotFound _pid -> H.p (H.text "Post not found") + +-- | Rendering of a single blog post +instance ToMarkup BlogPost where + toMarkup post = + H.div $ do + H.div $ H.h2 $ H.toHtml post.title + H.div $ H.p $ H.toHtml ("Created at: " <> show post.createdAt) + H.div H.! HA.style "white-space: pre-wrap" $ + H.text post.content + +-- Rendering of a single quote +instance ToMarkup Quote where + toMarkup quote = do + H.div $ H.h2 "Quote of the day:" + H.div $ H.p $ H.text quote.content + +-- | Rendering of all submited posts +instance ToMarkup ListPosts where + toMarkup (ListPosts posts) = + H.div $ do + H.h2 $ H.text "Posts:" + H.ul $ mapM_ (\p -> H.li $ toPostSummary p) $ List.sortOn (.createdAt) posts + where + toPostSummary post = + H.a H.! HA.href (renderUrl $ urls.blogPost $ Optional $ Just post.id) $ + H.text $ + post.title diff --git a/examples/mig-example-apps/Makefile b/examples/mig-example-apps/Makefile index 6963188..deb1cb0 100644 --- a/examples/mig-example-apps/Makefile +++ b/examples/mig-example-apps/Makefile @@ -7,7 +7,7 @@ test: stack test run: - stack run html-mig-example-app + stack run html-template-mig-example-app ## servers @@ -26,6 +26,9 @@ run-json-api: run-html: stack run html-mig-example-app +run-html-template: + stack run html-template-mig-example-app + ## clients run-counter: diff --git a/examples/mig-example-apps/mig-example-apps.cabal b/examples/mig-example-apps/mig-example-apps.cabal index 12b3dac..fccfba1 100644 --- a/examples/mig-example-apps/mig-example-apps.cabal +++ b/examples/mig-example-apps/mig-example-apps.cabal @@ -20,6 +20,9 @@ extra-source-files: Html/resources/haskell-logo.png Html/resources/lambda-logo.png Html/resources/milligram.min.css + HtmlTemplate/resources/haskell-logo.png + HtmlTemplate/resources/lambda-logo.png + HtmlTemplate/resources/milligram.min.css source-repository head type: git @@ -269,6 +272,64 @@ executable html-mig-example-app , uuid default-language: GHC2021 +executable html-template-mig-example-app + main-is: Main.hs + other-modules: + Api + Content + Init + Interface + Internal.State + Server + Types + View + Paths_mig_example_apps + hs-source-dirs: + HtmlTemplate/src + default-extensions: + ImportQualifiedPost + OverloadedStrings + TypeFamilies + OverloadedRecordDot + DuplicateRecordFields + LambdaCase + DerivingStrategies + DataKinds + StrictData + DeriveAnyClass + RecordWildCards + TemplateHaskell + StandaloneDeriving + DeriveGeneric + DeriveDataTypeable + GeneralizedNewtypeDeriving + ghc-options: -Wall -Werror -Wcompat -Widentities -Wincomplete-record-updates -Wincomplete-uni-patterns -Wmissing-export-lists -Wmissing-home-modules -Wpartial-fields -Wredundant-constraints -threaded -rtsopts -with-rtsopts=-N + build-depends: + aeson + , aeson-pretty + , base >=4.7 && <5 + , blaze-html + , bytestring + , containers + , derive-topdown + , fast-logger + , file-embed-lzma + , http-api-data + , http-types + , mig + , mig-client + , mig-extra + , mig-server + , mig-swagger-ui + , openapi3 + , pretty-simple + , random + , safe + , text + , time + , uuid + default-language: GHC2021 + executable json-api-mig-example-app main-is: Main.hs other-modules: diff --git a/examples/mig-example-apps/package.yaml b/examples/mig-example-apps/package.yaml index c8cfaf3..2d80187 100644 --- a/examples/mig-example-apps/package.yaml +++ b/examples/mig-example-apps/package.yaml @@ -9,6 +9,7 @@ copyright: "2023 Author name here" extra-source-files: - README.md - Html/resources/* +- HtmlTemplate/resources/* # Metadata used when publishing your package # synopsis: Short description of your package @@ -156,3 +157,18 @@ executables: - uuid - http-api-data - fast-logger + + html-template-mig-example-app: + main: Main.hs + source-dirs: HtmlTemplate/src + ghc-options: + - -threaded + - -rtsopts + - -with-rtsopts=-N + dependencies: + - blaze-html + - file-embed-lzma + - uuid + - http-api-data + - fast-logger + diff --git a/mig-client/src/Mig/Client.hs b/mig-client/src/Mig/Client.hs index e2d26a8..960b268 100644 --- a/mig-client/src/Mig/Client.hs +++ b/mig-client/src/Mig/Client.hs @@ -39,11 +39,6 @@ import Web.HttpApiData import Mig.Core -{-| Infox synonym for pair. It can be useful to stack together -many client functions in the output of @toClient@ function. --} -data (:|) a b = a :| b - instance (ToClient a, ToClient b) => ToClient (a :| b) where toClient api = a :| b where diff --git a/mig-extra/src/Mig/Extra/Server/Common.hs b/mig-extra/src/Mig/Extra/Server/Common.hs index acd56d6..84f6d34 100644 --- a/mig-extra/src/Mig/Extra/Server/Common.hs +++ b/mig-extra/src/Mig/Extra/Server/Common.hs @@ -41,6 +41,13 @@ module Mig.Extra.Server.Common ( HEAD, TRACE, + -- ** safe URLs + UrlOf, + ToUrl (..), + Url (..), + renderUrl, + (:|) (..), + -- ** path and query -- | Build API for routes with queries and captures. diff --git a/mig-extra/src/Mig/Extra/Server/Html.hs b/mig-extra/src/Mig/Extra/Server/Html.hs index e9d28d1..dfca8f6 100644 --- a/mig-extra/src/Mig/Extra/Server/Html.hs +++ b/mig-extra/src/Mig/Extra/Server/Html.hs @@ -28,7 +28,7 @@ import Mig.Extra.Server.Common as X -- response newtype Resp a = Resp (Core.Resp Html a) - deriving newtype (IsResp) + deriving newtype (IsResp, Eq, Show, Functor) type RespOr err a = Either (Resp err) (Resp a) diff --git a/mig-extra/src/Mig/Extra/Server/Json.hs b/mig-extra/src/Mig/Extra/Server/Json.hs index a37647c..51b8753 100644 --- a/mig-extra/src/Mig/Extra/Server/Json.hs +++ b/mig-extra/src/Mig/Extra/Server/Json.hs @@ -41,10 +41,10 @@ import Mig.Extra.Server.Common as X -- response newtype Resp a = Resp (Core.Resp Json a) - deriving newtype (IsResp, Functor) + deriving newtype (IsResp, Eq, Show, Functor) newtype RespOr err a = RespOr (Core.RespOr Json err a) - deriving newtype (IsResp, Functor) + deriving newtype (IsResp, Eq, Show, Functor) -- request diff --git a/mig-server/src/Mig.hs b/mig-server/src/Mig.hs index 9944cff..eb44ffa 100644 --- a/mig-server/src/Mig.hs +++ b/mig-server/src/Mig.hs @@ -70,6 +70,13 @@ module Mig ( HEAD, TRACE, + -- ** safe URLs + UrlOf, + ToUrl (..), + Url (..), + renderUrl, + (:|) (..), + -- ** path and query -- | Build API for routes with queries and captures. diff --git a/mig/mig.cabal b/mig/mig.cabal index 9f9283c..d2a4e62 100644 --- a/mig/mig.cabal +++ b/mig/mig.cabal @@ -47,6 +47,7 @@ library Mig.Core.Class.Response Mig.Core.Class.Route Mig.Core.Class.Server + Mig.Core.Class.Url Mig.Core.OpenApi Mig.Core.Server Mig.Core.Server.Cache @@ -54,6 +55,7 @@ library Mig.Core.Types Mig.Core.Types.Http Mig.Core.Types.Info + Mig.Core.Types.Pair Mig.Core.Types.Route other-modules: Paths_mig diff --git a/mig/src/Mig/Core/Class.hs b/mig/src/Mig/Core/Class.hs index 04cbcad..a4f250b 100644 --- a/mig/src/Mig/Core/Class.hs +++ b/mig/src/Mig/Core/Class.hs @@ -9,3 +9,4 @@ import Mig.Core.Class.Plugin as X import Mig.Core.Class.Response as X import Mig.Core.Class.Route as X import Mig.Core.Class.Server as X +import Mig.Core.Class.Url as X diff --git a/mig/src/Mig/Core/Class/Url.hs b/mig/src/Mig/Core/Class/Url.hs new file mode 100644 index 0000000..7c5b0d1 --- /dev/null +++ b/mig/src/Mig/Core/Class/Url.hs @@ -0,0 +1,139 @@ +module Mig.Core.Class.Url ( + Url (..), + UrlOf, + renderUrl, + ToUrl (..), +) where + +import Data.Bifunctor +import Data.Kind +import Data.Map.Strict (Map) +import Data.Map.Strict qualified as Map +import Data.Maybe +import Data.Proxy +import Data.String +import Data.Text (Text) +import Data.Text qualified as Text +import GHC.TypeLits +import Mig.Core.Api (Path (..), PathItem (..), flatApi, fromFlatApi) +import Mig.Core.Server (Server (..), getServerPaths) +import Mig.Core.Types.Pair +import Mig.Core.Types.Route +import Web.HttpApiData + +data Url = Url + { path :: Path + , queries :: [(Text, Text)] + , captures :: Map Text Text + } + +-- | TODO: use Text.Builder +renderUrl :: (IsString a) => Url -> a +renderUrl url = + fromString $ Text.unpack $ appendQuery $ mappend "/" $ Text.intercalate "/" $ fmap fromPathItem url.path.unPath + where + fromPathItem :: PathItem -> Text + fromPathItem = \case + StaticPath text -> text + CapturePath name -> fromMaybe ("{" <> name <> "}") $ Map.lookup name url.captures + + appendQuery = case url.queries of + [] -> id + _ -> \res -> mconcat [res, "?", query] + + query = Text.intercalate "&" $ fmap (\(name, val) -> mconcat [name, "=", val]) url.queries + +------------------------------------------------------------------------------------- +-- render routes to safe URLs + +-- | Converts route type to URL function +type family UrlOf a :: Type where + UrlOf (Send method m a) = Url + UrlOf (a -> b) = (a -> UrlOf b) + UrlOf (a, b) = (UrlOf a, UrlOf b) + UrlOf (a, b, c) = (UrlOf a, UrlOf b, UrlOf c) + UrlOf (a, b, c, d) = (UrlOf a, UrlOf b, UrlOf c, UrlOf d) + UrlOf (a, b, c, d, e) = (UrlOf a, UrlOf b, UrlOf c, UrlOf d, UrlOf e) + UrlOf (a, b, c, d, e, f) = (UrlOf a, UrlOf b, UrlOf c, UrlOf d, UrlOf e, UrlOf f) + +-- | Converts server to safe url +class ToUrl a where + toUrl :: Server m -> a + mapUrl :: (Url -> Url) -> a -> a + urlArity :: Int + +instance (ToUrl a, ToUrl b) => ToUrl (a :| b) where + toUrl api = a :| b + where + (a, b) = toUrl api + mapUrl f (a :| b) = (mapUrl f a :| mapUrl f b) + urlArity = urlArity @(a, b) + +instance (ToUrl a, ToUrl b) => ToUrl (a, b) where + toUrl (Server api) = (toUrl (Server apiA), toUrl (Server apiB)) + where + (apiA, apiB) = bimap fromFlatApi fromFlatApi $ Prelude.splitAt (urlArity @a) (flatApi api) + + mapUrl f (a, b) = (mapUrl f a, mapUrl f b) + + urlArity = urlArity @a + urlArity @b + +instance ToUrl Url where + toUrl server = case getServerPaths server of + url : _ -> Url url [] mempty + _ -> Url mempty mempty mempty + + mapUrl f a = f a + + urlArity = 1 + +-- query + +instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Query sym a -> b) where + toUrl server = \(Query val) -> + mapUrl (insertQuery (getName @sym) (toUrlPiece val)) (toUrl @b server) + + mapUrl f a = \query -> mapUrl f (a query) + + urlArity = urlArity @b + +insertQuery :: Text -> Text -> Url -> Url +insertQuery name val url = url{queries = (name, val) : url.queries} + +-- optional query + +instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Optional sym a -> b) where + toUrl server = \(Optional mVal) -> + mapUrl (maybe id (insertQuery (getName @sym) . toUrlPiece) mVal) (toUrl @b server) + + mapUrl f a = \query -> mapUrl f (a query) + + urlArity = urlArity @b + +-- capture + +instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Capture sym a -> b) where + toUrl server = \(Capture val) -> + mapUrl (insertCapture (getName @sym) (toUrlPiece val)) (toUrl @b server) + + mapUrl f a = \capture -> mapUrl f (a capture) + + urlArity = urlArity @b + +insertCapture :: Text -> Text -> Url -> Url +insertCapture name val url = url{captures = Map.insert name val url.captures} + +-- body + +instance (ToUrl b) => ToUrl (Body media a -> b) where + toUrl server = const $ toUrl @b server + + mapUrl f a = \body -> mapUrl f (a body) + + urlArity = urlArity @b + +------------------------------------------------------------------------------------- +-- utils + +getName :: forall sym a. (KnownSymbol sym, IsString a) => a +getName = fromString (symbolVal (Proxy @sym)) diff --git a/mig/src/Mig/Core/Types.hs b/mig/src/Mig/Core/Types.hs index 08c125a..3a413dc 100644 --- a/mig/src/Mig/Core/Types.hs +++ b/mig/src/Mig/Core/Types.hs @@ -7,4 +7,5 @@ module Mig.Core.Types ( import Mig.Core.Types.Http as X import Mig.Core.Types.Info as X +import Mig.Core.Types.Pair as X import Mig.Core.Types.Route as X diff --git a/mig/src/Mig/Core/Types/Pair.hs b/mig/src/Mig/Core/Types/Pair.hs new file mode 100644 index 0000000..248d850 --- /dev/null +++ b/mig/src/Mig/Core/Types/Pair.hs @@ -0,0 +1,8 @@ +module Mig.Core.Types.Pair ( + (:|) (..), +) where + +{-| Infox synonym for pair. It can be useful to stack together +many client functions in the output of @toClient@ function. +-} +data (:|) a b = a :| b From bcdac09498acb85d5a66a6d2583fef63cec2d36f Mon Sep 17 00:00:00 2001 From: Anton Kholomiov Date: Mon, 20 Nov 2023 23:25:22 +0300 Subject: [PATCH 2/3] Complete Url module --- mig/src/Mig/Core/Class/Url.hs | 61 +++++++++++++++++++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/mig/src/Mig/Core/Class/Url.hs b/mig/src/Mig/Core/Class/Url.hs index 7c5b0d1..99f3f06 100644 --- a/mig/src/Mig/Core/Class/Url.hs +++ b/mig/src/Mig/Core/Class/Url.hs @@ -110,6 +110,16 @@ instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Optional sym a -> urlArity = urlArity @b +-- query flag + +instance (KnownSymbol sym, ToUrl b) => ToUrl (QueryFlag sym -> b) where + toUrl server = \(QueryFlag val) -> + mapUrl (insertQuery (getName @sym) (toUrlPiece val)) (toUrl @b server) + + mapUrl f a = \query -> mapUrl f (a query) + + urlArity = urlArity @b + -- capture instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Capture sym a -> b) where @@ -132,6 +142,57 @@ instance (ToUrl b) => ToUrl (Body media a -> b) where urlArity = urlArity @b +-- header + +instance (ToUrl b) => ToUrl (Header sym a -> b) where + toUrl server = const $ toUrl @b server + + mapUrl f a = \header -> mapUrl f (a header) + + urlArity = urlArity @b + +-- optional header + +instance (ToUrl b) => ToUrl (OptionalHeader sym a -> b) where + toUrl server = const $ toUrl @b server + + mapUrl f a = \header -> mapUrl f (a header) + + urlArity = urlArity @b + +-- cookie + +instance (ToUrl b) => ToUrl (Cookie a -> b) where + toUrl server = const $ toUrl @b server + + mapUrl f a = \header -> mapUrl f (a header) + + urlArity = urlArity @b + +-- path info +instance (ToUrl b) => ToUrl (PathInfo -> b) where + toUrl server = const $ toUrl @b server + mapUrl f a = \input -> mapUrl f (a input) + urlArity = urlArity @b + +-- full path info +instance (ToUrl b) => ToUrl (FullPathInfo -> b) where + toUrl server = const $ toUrl @b server + mapUrl f a = \input -> mapUrl f (a input) + urlArity = urlArity @b + +-- request +instance (ToUrl b) => ToUrl (RawRequest -> b) where + toUrl server = const $ toUrl @b server + mapUrl f a = \input -> mapUrl f (a input) + urlArity = urlArity @b + +-- is secure +instance (ToUrl b) => ToUrl (IsSecure -> b) where + toUrl server = const $ toUrl @b server + mapUrl f a = \input -> mapUrl f (a input) + urlArity = urlArity @b + ------------------------------------------------------------------------------------- -- utils From c2fcd8e5703a61e178cb5db6541fdbf96775b268 Mon Sep 17 00:00:00 2001 From: Anton Kholomiov Date: Mon, 20 Nov 2023 23:35:46 +0300 Subject: [PATCH 3/3] Add comments --- mig/src/Mig/Core/Class/Url.hs | 58 ++++++++++++++++++++++++---------- mig/src/Mig/Core/Types/Pair.hs | 3 +- 2 files changed, 44 insertions(+), 17 deletions(-) diff --git a/mig/src/Mig/Core/Class/Url.hs b/mig/src/Mig/Core/Class/Url.hs index 99f3f06..e11c86e 100644 --- a/mig/src/Mig/Core/Class/Url.hs +++ b/mig/src/Mig/Core/Class/Url.hs @@ -21,13 +21,20 @@ import Mig.Core.Types.Pair import Mig.Core.Types.Route import Web.HttpApiData +-- | Url-template type. data Url = Url { path :: Path + -- ^ relative path , queries :: [(Text, Text)] + -- ^ queries in the URL , captures :: Map Text Text + -- ^ map of captures } --- | TODO: use Text.Builder +{-| Render URL to string-like value. + +TODO: use Text.Builder +-} renderUrl :: (IsString a) => Url -> a renderUrl url = fromString $ Text.unpack $ appendQuery $ mappend "/" $ Text.intercalate "/" $ fmap fromPathItem url.path.unPath @@ -56,7 +63,40 @@ type family UrlOf a :: Type where UrlOf (a, b, c, d, e) = (UrlOf a, UrlOf b, UrlOf c, UrlOf d, UrlOf e) UrlOf (a, b, c, d, e, f) = (UrlOf a, UrlOf b, UrlOf c, UrlOf d, UrlOf e, UrlOf f) --- | Converts server to safe url +{-| Converts server to safe url. We can use it to generate +safe URL constructors to be used in HTML templates +An example of how we can create safe URL's. Note +that order of URL's should be the same as in server definition: + +> type GreetingRoute = Get Html +> type BlogPostRoute = Optional "id" BlogPostId -> Get Html +> type ListPostsRoute = Get Html +> +> data Routes = Routes +> { greeting :: GreetingRoute +> , blogPost :: BlogPostRoute +> , listPosts :: ListPostsRoute +> } +> +> -- URLs +> +> data Urls = Urls +> { greeting :: UrlOf GreetingRoute +> , blogPost :: UrlOf BlogPostRoute +> , listPosts :: UrlOf ListPostsRoute +> } +> +> {\-| Site URL's +> URL's should be listed in the same order as they appear in the server +> -\} +> urls :: Urls +> urls = Urls{..} +> where +> greeting +> :| blogPost +> :| listPosts +> toUrl (server undefined) +-} class ToUrl a where toUrl :: Server m -> a mapUrl :: (Url -> Url) -> a -> a @@ -75,7 +115,6 @@ instance (ToUrl a, ToUrl b) => ToUrl (a, b) where (apiA, apiB) = bimap fromFlatApi fromFlatApi $ Prelude.splitAt (urlArity @a) (flatApi api) mapUrl f (a, b) = (mapUrl f a, mapUrl f b) - urlArity = urlArity @a + urlArity @b instance ToUrl Url where @@ -84,7 +123,6 @@ instance ToUrl Url where _ -> Url mempty mempty mempty mapUrl f a = f a - urlArity = 1 -- query @@ -94,7 +132,6 @@ instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Query sym a -> b) mapUrl (insertQuery (getName @sym) (toUrlPiece val)) (toUrl @b server) mapUrl f a = \query -> mapUrl f (a query) - urlArity = urlArity @b insertQuery :: Text -> Text -> Url -> Url @@ -107,7 +144,6 @@ instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Optional sym a -> mapUrl (maybe id (insertQuery (getName @sym) . toUrlPiece) mVal) (toUrl @b server) mapUrl f a = \query -> mapUrl f (a query) - urlArity = urlArity @b -- query flag @@ -117,7 +153,6 @@ instance (KnownSymbol sym, ToUrl b) => ToUrl (QueryFlag sym -> b) where mapUrl (insertQuery (getName @sym) (toUrlPiece val)) (toUrl @b server) mapUrl f a = \query -> mapUrl f (a query) - urlArity = urlArity @b -- capture @@ -127,7 +162,6 @@ instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Capture sym a -> mapUrl (insertCapture (getName @sym) (toUrlPiece val)) (toUrl @b server) mapUrl f a = \capture -> mapUrl f (a capture) - urlArity = urlArity @b insertCapture :: Text -> Text -> Url -> Url @@ -137,36 +171,28 @@ insertCapture name val url = url{captures = Map.insert name val url.captures} instance (ToUrl b) => ToUrl (Body media a -> b) where toUrl server = const $ toUrl @b server - mapUrl f a = \body -> mapUrl f (a body) - urlArity = urlArity @b -- header instance (ToUrl b) => ToUrl (Header sym a -> b) where toUrl server = const $ toUrl @b server - mapUrl f a = \header -> mapUrl f (a header) - urlArity = urlArity @b -- optional header instance (ToUrl b) => ToUrl (OptionalHeader sym a -> b) where toUrl server = const $ toUrl @b server - mapUrl f a = \header -> mapUrl f (a header) - urlArity = urlArity @b -- cookie instance (ToUrl b) => ToUrl (Cookie a -> b) where toUrl server = const $ toUrl @b server - mapUrl f a = \header -> mapUrl f (a header) - urlArity = urlArity @b -- path info diff --git a/mig/src/Mig/Core/Types/Pair.hs b/mig/src/Mig/Core/Types/Pair.hs index 248d850..be95d8e 100644 --- a/mig/src/Mig/Core/Types/Pair.hs +++ b/mig/src/Mig/Core/Types/Pair.hs @@ -1,8 +1,9 @@ +-- | Pair with infix constructor. module Mig.Core.Types.Pair ( (:|) (..), ) where -{-| Infox synonym for pair. It can be useful to stack together +{-| Infix synonym for pair. It can be useful to stack together many client functions in the output of @toClient@ function. -} data (:|) a b = a :| b