diff --git a/examples/mig-example-apps/HtmlTemplate/templates/postNotFound.html b/examples/mig-example-apps/HtmlTemplate/templates/postNotFound.html
new file mode 100644
index 0000000..7efb5c7
--- /dev/null
+++ b/examples/mig-example-apps/HtmlTemplate/templates/postNotFound.html
@@ -0,0 +1 @@
+
Post not found
diff --git a/examples/mig-example-apps/HtmlTemplate/templates/quote.html b/examples/mig-example-apps/HtmlTemplate/templates/quote.html
new file mode 100644
index 0000000..738e8ef
--- /dev/null
+++ b/examples/mig-example-apps/HtmlTemplate/templates/quote.html
@@ -0,0 +1,2 @@
+
Quote of the day:
+
{{content}}
diff --git a/examples/mig-example-apps/HtmlTemplate/templates/writeForm.html b/examples/mig-example-apps/HtmlTemplate/templates/writeForm.html
new file mode 100644
index 0000000..1f0d8bb
--- /dev/null
+++ b/examples/mig-example-apps/HtmlTemplate/templates/writeForm.html
@@ -0,0 +1,19 @@
+
+
Write new post
+
+
+
diff --git a/examples/mig-example-apps/README.md b/examples/mig-example-apps/README.md
index b085e85..493e88b 100644
--- a/examples/mig-example-apps/README.md
+++ b/examples/mig-example-apps/README.md
@@ -13,6 +13,8 @@ We can find out how to build various servers:
* `Html` - simple blog post site that servers HTML.
+* `HtmlTemplate` - variation of `Html` example with safe URLs and HTML-templates
+
Also we can build clients:
* `HelloClient` - basic hello world client
diff --git a/examples/mig-example-apps/mig-example-apps.cabal b/examples/mig-example-apps/mig-example-apps.cabal
index fccfba1..cc5572c 100644
--- a/examples/mig-example-apps/mig-example-apps.cabal
+++ b/examples/mig-example-apps/mig-example-apps.cabal
@@ -7,26 +7,30 @@ cabal-version: 1.12
name: mig-example-apps
version: 0.1.0.0
description: Please see the README on GitHub at
-homepage: https://github.com/githubuser/mig-example-apps#readme
-bug-reports: https://github.com/githubuser/mig-example-apps/issues
-author: Author name here
-maintainer: example@example.com
-copyright: 2023 Author name here
+homepage: https://github.com/anton-k/mig#readme
+bug-reports: https://github.com/anton-k/mig/issues
+author: Anton Kholomiov
+maintainer: anton.kholomiov@gmail.com
+copyright: 2023 Anton Kholomiov
license: BSD3
license-file: LICENSE
build-type: Simple
extra-source-files:
README.md
- 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
+ HtmlTemplate/templates/greeting.html
+ HtmlTemplate/templates/listPosts.html
+ HtmlTemplate/templates/main.html
+ HtmlTemplate/templates/post.html
+ HtmlTemplate/templates/postNotFound.html
+ HtmlTemplate/templates/quote.html
+ HtmlTemplate/templates/writeForm.html
source-repository head
type: git
- location: https://github.com/githubuser/mig-example-apps
+ location: https://github.com/anton-k/mig
executable counter-client-mig-example-app
main-is: Main.hs
@@ -61,7 +65,6 @@ executable counter-client-mig-example-app
, base >=4.7 && <5
, bytestring
, containers
- , derive-topdown
, http-client
, http-types
, mig
@@ -108,7 +111,6 @@ executable counter-mig-example-app
, base >=4.7 && <5
, bytestring
, containers
- , derive-topdown
, http-types
, mig
, mig-client
@@ -154,7 +156,6 @@ executable hello-world-client-mig-example-app
, base >=4.7 && <5
, bytestring
, containers
- , derive-topdown
, http-client
, http-types
, mig
@@ -200,7 +201,6 @@ executable hello-world-mig-example-app
, base >=4.7 && <5
, bytestring
, containers
- , derive-topdown
, http-types
, mig
, mig-client
@@ -253,7 +253,6 @@ executable html-mig-example-app
, blaze-html
, bytestring
, containers
- , derive-topdown
, fast-logger
, file-embed-lzma
, http-api-data
@@ -309,9 +308,9 @@ executable html-template-mig-example-app
, aeson-pretty
, base >=4.7 && <5
, blaze-html
+ , blaze-markup
, bytestring
, containers
- , derive-topdown
, fast-logger
, file-embed-lzma
, http-api-data
@@ -325,6 +324,7 @@ executable html-template-mig-example-app
, pretty-simple
, random
, safe
+ , stache
, text
, time
, uuid
@@ -366,7 +366,6 @@ executable json-api-mig-example-app
, base >=4.7 && <5
, bytestring
, containers
- , derive-topdown
, fast-logger
, http-types
, mig
@@ -413,7 +412,6 @@ executable route-args-client-mig-example-app
, base >=4.7 && <5
, bytestring
, containers
- , derive-topdown
, http-client
, http-types
, mig
@@ -459,7 +457,6 @@ executable route-args-mig-example-app
, base >=4.7 && <5
, bytestring
, containers
- , derive-topdown
, http-types
, mig
, mig-client
diff --git a/examples/mig-example-apps/package.yaml b/examples/mig-example-apps/package.yaml
index 2d80187..5fef2ac 100644
--- a/examples/mig-example-apps/package.yaml
+++ b/examples/mig-example-apps/package.yaml
@@ -1,15 +1,15 @@
name: mig-example-apps
version: 0.1.0.0
-github: "githubuser/mig-example-apps"
+github: "anton-k/mig"
license: BSD3
-author: "Author name here"
-maintainer: "example@example.com"
-copyright: "2023 Author name here"
+author: "Anton Kholomiov"
+maintainer: "anton.kholomiov@gmail.com"
+copyright: "2023 Anton Kholomiov"
extra-source-files:
- README.md
-- Html/resources/*
- HtmlTemplate/resources/*
+- HtmlTemplate/templates/*
# Metadata used when publishing your package
# synopsis: Short description of your package
@@ -72,7 +72,6 @@ dependencies:
- safe
- containers
- http-types
- - derive-topdown
executables:
hello-world-mig-example-app:
@@ -171,4 +170,6 @@ executables:
- uuid
- http-api-data
- fast-logger
+ - stache
+ - blaze-markup
diff --git a/mig-extra/src/Mig/Extra/Server/Html.hs b/mig-extra/src/Mig/Extra/Server/Html.hs
index dfca8f6..8305d8f 100644
--- a/mig-extra/src/Mig/Extra/Server/Html.hs
+++ b/mig-extra/src/Mig/Extra/Server/Html.hs
@@ -1,3 +1,4 @@
+{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Html servers
@@ -16,6 +17,9 @@ module Mig.Extra.Server.Html (
Resp (..),
RespOr,
+ -- * utils
+ Link (..),
+
-- * re-exports
Body (..),
module X,
@@ -24,6 +28,8 @@ module Mig.Extra.Server.Html (
import Mig.Core (Body (..))
import Mig.Core qualified as Core
import Mig.Extra.Server.Common as X
+import Text.Blaze.Html5 qualified as H
+import Text.Blaze.Html5.Attributes qualified as HA
-- response
@@ -40,3 +46,16 @@ type Patch m a = Send PATCH m (Resp a)
type Options m a = Send OPTIONS m (Resp a)
type Head m a = Send HEAD m (Resp a)
type Trace m a = Send TRACE m (Resp a)
+
+{-| HTML a-links, this type is useful for using
+with template engines that rely on @ToJSON@ instance.
+Also it can be rendered as Html with @ToMarkup@ instance.
+-}
+data Link = Link
+ { href :: Url
+ , name :: Text
+ }
+ deriving (Generic, ToJSON)
+
+instance ToMarkup Link where
+ toMarkup link = H.a H.! HA.href (renderUrl link.href) $ H.text link.name
diff --git a/mig-extra/src/Mig/Extra/Server/Html/IO.hs b/mig-extra/src/Mig/Extra/Server/Html/IO.hs
index 6850c3e..6d7a2b5 100644
--- a/mig-extra/src/Mig/Extra/Server/Html/IO.hs
+++ b/mig-extra/src/Mig/Extra/Server/Html/IO.hs
@@ -14,6 +14,9 @@ module Mig.Extra.Server.Html.IO (
Resp (..),
RespOr,
+ -- * utils
+ Link (..),
+
-- * re-exports
Body (..),
module X,
@@ -21,7 +24,7 @@ module Mig.Extra.Server.Html.IO (
import Mig.Core (Body (..))
import Mig.Extra.Server.Common as X
-import Mig.Extra.Server.Html (Resp (..), RespOr)
+import Mig.Extra.Server.Html (Link (..), Resp (..), RespOr)
type Get a = Send GET IO (Resp a)
type Post a = Send POST IO (Resp a)
diff --git a/mig/src/Mig/Core/Class/Url.hs b/mig/src/Mig/Core/Class/Url.hs
index bd661ec..c5f426a 100644
--- a/mig/src/Mig/Core/Class/Url.hs
+++ b/mig/src/Mig/Core/Class/Url.hs
@@ -5,6 +5,7 @@ module Mig.Core.Class.Url (
ToUrl (..),
) where
+import Data.Aeson (ToJSON (..))
import Data.Bifunctor
import Data.Kind
import Data.Map.Strict (Map)
@@ -31,6 +32,9 @@ data Url = Url
-- ^ map of captures
}
+instance ToJSON Url where
+ toJSON = toJSON . renderUrl @Text
+
{-| Render URL to string-like value.
TODO: use Text.Builder
@@ -56,7 +60,18 @@ renderUrl url =
-- | 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 (Query name value -> b) = (Query name value -> UrlOf b)
+ UrlOf (Optional name value -> b) = (Optional name value -> UrlOf b)
+ UrlOf (Capture name value -> b) = (Capture name value -> UrlOf b)
+ UrlOf (QueryFlag name -> b) = (QueryFlag name -> UrlOf b)
+ UrlOf (Header name value -> b) = UrlOf b
+ UrlOf (OptionalHeader name value -> b) = UrlOf b
+ UrlOf (Body media value -> b) = UrlOf b
+ UrlOf (Cookie value -> b) = UrlOf b
+ UrlOf (PathInfo -> b) = UrlOf b
+ UrlOf (FullPathInfo -> b) = UrlOf b
+ UrlOf (RawRequest -> b) = UrlOf b
+ UrlOf (IsSecure -> b) = 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)
@@ -118,6 +133,22 @@ instance (ToUrl a, ToUrl b) => ToUrl (a, b) where
mapUrl f (a, b) = (mapUrl f a, mapUrl f b)
urlArity = urlArity @a + urlArity @b
+instance (ToUrl a, ToUrl b, ToUrl c) => ToUrl (a, b, c) where
+ toUrl server = fromPair $ toUrl @(a, (b, c)) server
+ where
+ fromPair (a, (b, c)) = (a, b, c)
+
+ mapUrl f (a, b, c) = (mapUrl f a, mapUrl f b, mapUrl f c)
+ urlArity = urlArity @a + urlArity @b + urlArity @c
+
+instance (ToUrl a, ToUrl b, ToUrl c, ToUrl d) => ToUrl (a, b, c, d) where
+ toUrl server = fromPair $ toUrl @(a, (b, c, d)) server
+ where
+ fromPair (a, (b, c, d)) = (a, b, c, d)
+
+ mapUrl f (a, b, c, d) = (mapUrl f a, mapUrl f b, mapUrl f c, mapUrl f d)
+ urlArity = urlArity @a + urlArity @b + urlArity @c + urlArity @d
+
instance ToUrl Url where
toUrl server = case getServerPaths server of
url : _ -> Url url [] mempty
@@ -168,58 +199,6 @@ instance (KnownSymbol sym, ToHttpApiData a, ToUrl b) => ToUrl (Capture sym a ->
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
-
--- 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