mirror of
https://github.com/srid/ema.git
synced 2024-11-29 09:25:14 +03:00
add some encoders, and a new example WIP
This commit is contained in:
parent
aea64d32cb
commit
c0f1c8080a
1
.hlint.yaml
Normal file
1
.hlint.yaml
Normal file
@ -0,0 +1 @@
|
|||||||
|
- ignore: {name: Use camelCase }
|
@ -138,7 +138,8 @@ library
|
|||||||
if flag(with-examples)
|
if flag(with-examples)
|
||||||
other-modules:
|
other-modules:
|
||||||
Ema.Example.Common
|
Ema.Example.Common
|
||||||
Ema.Example.Ex02_Basic
|
Ema.Example.Ex01_Basic
|
||||||
|
Ema.Example.Ex02_Bookshelf
|
||||||
Ema.Example.Ex03_Clock
|
Ema.Example.Ex03_Clock
|
||||||
Ema.Example.Ex04_Multi
|
Ema.Example.Ex04_Multi
|
||||||
|
|
||||||
|
40
src/Ema/Example/Ex01_Basic.hs
Normal file
40
src/Ema/Example/Ex01_Basic.hs
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
|
||||||
|
-- | A very simple site in three parts: route types, `main` and rendering implementation.
|
||||||
|
module Ema.Example.Ex01_Basic where
|
||||||
|
|
||||||
|
import Ema
|
||||||
|
import Ema.Example.Common (tailwindLayout)
|
||||||
|
import Generics.SOP qualified as SOP
|
||||||
|
import Text.Blaze.Html5 ((!))
|
||||||
|
import Text.Blaze.Html5 qualified as H
|
||||||
|
import Text.Blaze.Html5.Attributes qualified as A
|
||||||
|
|
||||||
|
data Route
|
||||||
|
= Route_Index
|
||||||
|
| Route_About
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, HasModel, IsRoute)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = void $ Ema.runSite @Route ()
|
||||||
|
|
||||||
|
instance RenderAsset Route where
|
||||||
|
renderAsset enc () r =
|
||||||
|
Ema.AssetGenerated Ema.Html $
|
||||||
|
tailwindLayout (H.title "Basic site" >> H.base ! A.href "/") $
|
||||||
|
H.div ! A.class_ "container mx-auto mt-8 p-2" $ do
|
||||||
|
H.h1 ! A.class_ "text-3xl font-bold" $ "Basic site"
|
||||||
|
case r of
|
||||||
|
Route_Index -> do
|
||||||
|
"You are on the index page. "
|
||||||
|
routeElem Route_About "Go to About"
|
||||||
|
Route_About -> do
|
||||||
|
routeElem Route_Index "Go to Index"
|
||||||
|
". You are on the about page. "
|
||||||
|
where
|
||||||
|
routeElem r' w =
|
||||||
|
H.a ! A.class_ "text-red-500 hover:underline" ! routeHref r' $ w
|
||||||
|
routeHref r' =
|
||||||
|
A.href (fromString . toString $ Ema.routeUrl enc () r')
|
@ -1,46 +0,0 @@
|
|||||||
{-# LANGUAGE DeriveAnyClass #-}
|
|
||||||
{-# LANGUAGE UndecidableInstances #-}
|
|
||||||
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
|
|
||||||
|
|
||||||
{-# HLINT ignore "Use camelCase" #-}
|
|
||||||
|
|
||||||
-- | A very simple site with two routes, and HTML rendered using Blaze DSL
|
|
||||||
module Ema.Example.Ex02_Basic where
|
|
||||||
|
|
||||||
import Ema
|
|
||||||
import Ema.Example.Common (tailwindLayout)
|
|
||||||
import Ema.Route.Class (IsRoute)
|
|
||||||
import GHC.Generics qualified as GHC
|
|
||||||
import Generics.SOP (Generic, HasDatatypeInfo)
|
|
||||||
import Text.Blaze.Html5 ((!))
|
|
||||||
import Text.Blaze.Html5 qualified as H
|
|
||||||
import Text.Blaze.Html5.Attributes qualified as A
|
|
||||||
import Prelude hiding (Generic)
|
|
||||||
|
|
||||||
data Route
|
|
||||||
= Route_Index
|
|
||||||
| Route_About
|
|
||||||
deriving stock (Show, Eq, GHC.Generic)
|
|
||||||
deriving anyclass (Generic, HasDatatypeInfo, IsRoute, HasModel)
|
|
||||||
|
|
||||||
main :: IO ()
|
|
||||||
main = void $ Ema.runSite @Route ()
|
|
||||||
|
|
||||||
instance RenderAsset Route where
|
|
||||||
renderAsset enc () r =
|
|
||||||
Ema.AssetGenerated Ema.Html $
|
|
||||||
tailwindLayout (H.title "Basic site" >> H.base ! A.href "/") $
|
|
||||||
H.div ! A.class_ "container mx-auto" $ do
|
|
||||||
H.div ! A.class_ "mt-8 p-2 text-center" $ do
|
|
||||||
case r of
|
|
||||||
Route_Index -> do
|
|
||||||
"You are on the index page. "
|
|
||||||
routeElem Route_About "Go to About"
|
|
||||||
Route_About -> do
|
|
||||||
routeElem Route_Index "Go to Index"
|
|
||||||
". You are on the about page. "
|
|
||||||
where
|
|
||||||
routeElem r' w =
|
|
||||||
H.a ! A.class_ "text-red-500 hover:underline" ! routeHref r' $ w
|
|
||||||
routeHref r' =
|
|
||||||
A.href (fromString . toString $ Ema.routeUrl enc () r')
|
|
66
src/Ema/Example/Ex02_Bookshelf.hs
Normal file
66
src/Ema/Example/Ex02_Bookshelf.hs
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE UndecidableInstances #-}
|
||||||
|
|
||||||
|
-- | TODO: rewrite this to load books.json and display that, with individual page for books too.
|
||||||
|
module Ema.Example.Ex02_Bookshelf where
|
||||||
|
|
||||||
|
import Ema
|
||||||
|
import Ema.Example.Common (tailwindLayout)
|
||||||
|
import Generics.SOP qualified as SOP
|
||||||
|
import Text.Blaze.Html5 ((!))
|
||||||
|
import Text.Blaze.Html5 qualified as H
|
||||||
|
import Text.Blaze.Html5.Attributes qualified as A
|
||||||
|
|
||||||
|
data Route
|
||||||
|
= Route_Index
|
||||||
|
| Route_About
|
||||||
|
| Route_Products ProductRoute
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, HasModel, IsRoute)
|
||||||
|
|
||||||
|
-- TODO: Use DerivingVia to specify options, to disable extra /product/ in URL.
|
||||||
|
data ProductRoute
|
||||||
|
= ProductRoute_Index
|
||||||
|
| ProductRoute_Product ProductName
|
||||||
|
deriving stock (Show, Eq, Generic)
|
||||||
|
deriving anyclass (SOP.Generic, SOP.HasDatatypeInfo, IsRoute)
|
||||||
|
|
||||||
|
-- TODO: Demonstrate all_ using a model (that loads from .json?)
|
||||||
|
newtype ProductName = ProductName Text
|
||||||
|
deriving stock (Show, Eq)
|
||||||
|
deriving newtype (IsString, ToString, IsRoute)
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = void $ Ema.runSite @Route ()
|
||||||
|
|
||||||
|
instance RenderAsset Route where
|
||||||
|
renderAsset enc () r =
|
||||||
|
Ema.AssetGenerated Ema.Html $
|
||||||
|
tailwindLayout (H.title "Basic site" >> H.base ! A.href "/") $
|
||||||
|
H.div ! A.class_ "container mx-auto mt-8 p-2" $ do
|
||||||
|
H.h1 ! A.class_ "text-3xl font-bold" $ "TODO: Bookshelf"
|
||||||
|
case r of
|
||||||
|
Route_Index -> do
|
||||||
|
"You are on the index page. "
|
||||||
|
routeElem Route_About "Go to About"
|
||||||
|
" or go to "
|
||||||
|
routeElem (Route_Products ProductRoute_Index) "products"
|
||||||
|
Route_About -> do
|
||||||
|
routeElem Route_Index "Go to Index"
|
||||||
|
". You are on the about page. "
|
||||||
|
Route_Products pr -> do
|
||||||
|
H.h2 "Products"
|
||||||
|
case pr of
|
||||||
|
ProductRoute_Index -> do
|
||||||
|
H.p "List of products go here"
|
||||||
|
H.li $ routeElem (Route_Products $ ProductRoute_Product "egg") "Eggs"
|
||||||
|
H.li $ routeElem (Route_Products $ ProductRoute_Product "sausage") "Sausages"
|
||||||
|
routeElem Route_Index "Back to index"
|
||||||
|
ProductRoute_Product name -> do
|
||||||
|
H.h3 ! A.class_ "p-2 border-2" $ fromString . toString $ name
|
||||||
|
routeElem (Route_Products ProductRoute_Index) "Back to products"
|
||||||
|
where
|
||||||
|
routeElem r' w =
|
||||||
|
H.a ! A.class_ "text-red-500 hover:underline" ! routeHref r' $ w
|
||||||
|
routeHref r' =
|
||||||
|
A.href (fromString . toString $ Ema.routeUrl enc () r')
|
@ -8,7 +8,8 @@ module Ema.Example.Ex04_Multi where
|
|||||||
import Data.Generics.Sum.Any (AsAny (_As))
|
import Data.Generics.Sum.Any (AsAny (_As))
|
||||||
import Ema
|
import Ema
|
||||||
import Ema.Example.Common (tailwindLayout)
|
import Ema.Example.Common (tailwindLayout)
|
||||||
import Ema.Example.Ex02_Basic qualified as Ex02
|
import Ema.Example.Ex01_Basic qualified as Ex01
|
||||||
|
import Ema.Example.Ex02_Bookshelf qualified as Ex02
|
||||||
import Ema.Example.Ex03_Clock qualified as Ex03
|
import Ema.Example.Ex03_Clock qualified as Ex03
|
||||||
import GHC.Generics qualified as GHC
|
import GHC.Generics qualified as GHC
|
||||||
import Generics.SOP (Generic, HasDatatypeInfo, I (..), NP (..))
|
import Generics.SOP (Generic, HasDatatypeInfo, I (..), NP (..))
|
||||||
@ -19,7 +20,8 @@ import Prelude hiding (Generic)
|
|||||||
|
|
||||||
data R
|
data R
|
||||||
= R_Index
|
= R_Index
|
||||||
| R_Basic Ex02.Route
|
| R_Basic Ex01.Route
|
||||||
|
| R_Bookshelf Ex02.Route
|
||||||
| R_Clock Ex03.Route
|
| R_Clock Ex03.Route
|
||||||
deriving stock (Show, Eq, GHC.Generic)
|
deriving stock (Show, Eq, GHC.Generic)
|
||||||
deriving anyclass (Generic, HasDatatypeInfo, IsRoute)
|
deriving anyclass (Generic, HasDatatypeInfo, IsRoute)
|
||||||
@ -41,11 +43,11 @@ instance RenderAsset R where
|
|||||||
R_Index ->
|
R_Index ->
|
||||||
Ema.AssetGenerated Ema.Html $ renderIndex m
|
Ema.AssetGenerated Ema.Html $ renderIndex m
|
||||||
R_Basic r ->
|
R_Basic r ->
|
||||||
let enc' = innerRouteEncoder (_As @"R_Basic") enc
|
renderAsset (innerRouteEncoder (_As @"R_Basic") enc) (innerModel m) r
|
||||||
in renderAsset enc' (innerModel m) r
|
R_Bookshelf r ->
|
||||||
|
renderAsset (innerRouteEncoder (_As @"R_Bookshelf") enc) (innerModel m) r
|
||||||
R_Clock r ->
|
R_Clock r ->
|
||||||
let enc' = innerRouteEncoder (_As @"R_Clock") enc
|
renderAsset (innerRouteEncoder (_As @"R_Clock") enc) (innerModel m) r
|
||||||
in renderAsset enc' (innerModel m) r
|
|
||||||
|
|
||||||
renderIndex :: M -> LByteString
|
renderIndex :: M -> LByteString
|
||||||
renderIndex (I clockTime :* Nil) =
|
renderIndex (I clockTime :* Nil) =
|
||||||
@ -53,7 +55,8 @@ renderIndex (I clockTime :* Nil) =
|
|||||||
H.div ! A.class_ "container mx-auto text-center mt-8 p-2" $ do
|
H.div ! A.class_ "container mx-auto text-center mt-8 p-2" $ do
|
||||||
H.p "You can compose Ema sites. Here are two sites composed to produce one:"
|
H.p "You can compose Ema sites. Here are two sites composed to produce one:"
|
||||||
H.ul ! A.class_ "flex flex-col justify-center .items-center mt-4 space-y-4" $ do
|
H.ul ! A.class_ "flex flex-col justify-center .items-center mt-4 space-y-4" $ do
|
||||||
H.li $ routeElem "basic" "Ex02_Basic"
|
H.li $ routeElem "basic" "Ex01_Basic"
|
||||||
|
H.li $ routeElem "bookshelf" "Ex02_Bookshelf"
|
||||||
H.li $ routeElem "clock" "Ex03_Clock"
|
H.li $ routeElem "clock" "Ex03_Clock"
|
||||||
H.p $ do
|
H.p $ do
|
||||||
"The current time is: "
|
"The current time is: "
|
||||||
|
@ -3,14 +3,13 @@
|
|||||||
|
|
||||||
module Ema.Model
|
module Ema.Model
|
||||||
( HasModel (..),
|
( HasModel (..),
|
||||||
unitModel,
|
|
||||||
)
|
)
|
||||||
where
|
where
|
||||||
|
|
||||||
import Control.Monad.Logger (MonadLoggerIO)
|
import Control.Monad.Logger (MonadLoggerIO)
|
||||||
import Data.Some (Some)
|
import Data.Some (Some)
|
||||||
import Ema.CLI qualified as CLI
|
import Ema.CLI qualified as CLI
|
||||||
import Ema.Dynamic (Dynamic (Dynamic))
|
import Ema.Dynamic (Dynamic)
|
||||||
import Ema.Route.Class (IsRoute (RouteModel))
|
import Ema.Route.Class (IsRoute (RouteModel))
|
||||||
import Ema.Route.Encoder (RouteEncoder)
|
import Ema.Route.Encoder (RouteEncoder)
|
||||||
import UnliftIO (MonadUnliftIO)
|
import UnliftIO (MonadUnliftIO)
|
||||||
@ -36,12 +35,4 @@ class IsRoute r => HasModel r where
|
|||||||
RouteEncoder (RouteModel r) r ->
|
RouteEncoder (RouteModel r) r ->
|
||||||
ModelInput r ->
|
ModelInput r ->
|
||||||
m (Dynamic m (RouteModel r))
|
m (Dynamic m (RouteModel r))
|
||||||
runModel _ _ _ = pure unitModel
|
runModel _ _ _ = pure $ pure ()
|
||||||
|
|
||||||
unitModel :: Monad m => Dynamic m ()
|
|
||||||
unitModel =
|
|
||||||
Dynamic
|
|
||||||
( (),
|
|
||||||
\_set -> do
|
|
||||||
pure ()
|
|
||||||
)
|
|
||||||
|
@ -7,6 +7,8 @@ module Ema.Route.Class
|
|||||||
( IsRoute (RouteModel, mkRouteEncoder),
|
( IsRoute (RouteModel, mkRouteEncoder),
|
||||||
gMkRouteEncoder,
|
gMkRouteEncoder,
|
||||||
ConstModelRoute (..),
|
ConstModelRoute (..),
|
||||||
|
ShowReadable (ShowReadable),
|
||||||
|
Stringable (Stringable),
|
||||||
|
|
||||||
-- * Sub routes
|
-- * Sub routes
|
||||||
innerRouteEncoder,
|
innerRouteEncoder,
|
||||||
@ -60,6 +62,24 @@ instance
|
|||||||
mkRouteEncoder =
|
mkRouteEncoder =
|
||||||
gMkRouteEncoder @r & mapRouteEncoder (iso id Just) (prism' unConstModelRoute (Just . ConstModelRoute)) (const ())
|
gMkRouteEncoder @r & mapRouteEncoder (iso id Just) (prism' unConstModelRoute (Just . ConstModelRoute)) (const ())
|
||||||
|
|
||||||
|
newtype ShowReadable a = ShowReadable a
|
||||||
|
deriving newtype (Show, Read)
|
||||||
|
|
||||||
|
newtype Stringable a = Stringable a
|
||||||
|
deriving newtype (ToString, IsString)
|
||||||
|
|
||||||
|
instance (Show a, Read a) => IsRoute (ShowReadable a) where
|
||||||
|
type RouteModel (ShowReadable a) = ()
|
||||||
|
mkRouteEncoder = showReadRouteEncoder
|
||||||
|
|
||||||
|
instance (IsString a, ToString a) => IsRoute (Stringable a) where
|
||||||
|
type RouteModel (Stringable a) = ()
|
||||||
|
mkRouteEncoder = stringRouteEncoder
|
||||||
|
|
||||||
|
deriving via (Stringable Text) instance IsRoute Text
|
||||||
|
|
||||||
|
deriving via (Stringable String) instance IsRoute String
|
||||||
|
|
||||||
instance IsRoute () where
|
instance IsRoute () where
|
||||||
type RouteModel () = ()
|
type RouteModel () = ()
|
||||||
mkRouteEncoder = singletonRouteEncoder
|
mkRouteEncoder = singletonRouteEncoder
|
||||||
@ -83,6 +103,8 @@ class HasModel (xs :: [Type]) (x :: Type) where
|
|||||||
-- | Fill in the outter model containing the given inner model.
|
-- | Fill in the outter model containing the given inner model.
|
||||||
outerModel :: x -> NP I xs
|
outerModel :: x -> NP I xs
|
||||||
|
|
||||||
|
-- Could probably replace this lens-sop:
|
||||||
|
-- with https://hackage.haskell.org/package/lens-sop-0.2.0.3/docs/Generics-SOP-Lens.html#v:np
|
||||||
instance {-# OVERLAPPING #-} HasModel '[] () where
|
instance {-# OVERLAPPING #-} HasModel '[] () where
|
||||||
innerModel _ = ()
|
innerModel _ = ()
|
||||||
outerModel () = Nil
|
outerModel () = Nil
|
||||||
@ -103,14 +125,12 @@ instance {-# OVERLAPPABLE #-} HasModel xs x => HasModel (x' ': xs) x where
|
|||||||
innerModel (_ :* xs) = innerModel xs
|
innerModel (_ :* xs) = innerModel xs
|
||||||
outerModel x = I undefined :* outerModel x
|
outerModel x = I undefined :* outerModel x
|
||||||
|
|
||||||
-- TODO: avoid Iso using https://hackage.haskell.org/package/generic-lens
|
|
||||||
|
|
||||||
-- | Extract the inner RouteEncoder.
|
-- | Extract the inner RouteEncoder.
|
||||||
|
-- TODO: avoid having to specify Prism
|
||||||
innerRouteEncoder ::
|
innerRouteEncoder ::
|
||||||
forall m o i (ms :: [Type]).
|
forall m o i (ms :: [Type]).
|
||||||
HasModel ms m =>
|
HasModel ms m =>
|
||||||
Prism' o i ->
|
Prism' o i ->
|
||||||
-- Iso o o (Maybe i) i ->
|
|
||||||
RouteEncoder (NP I ms) o ->
|
RouteEncoder (NP I ms) o ->
|
||||||
RouteEncoder m i
|
RouteEncoder m i
|
||||||
innerRouteEncoder prism =
|
innerRouteEncoder prism =
|
||||||
|
@ -11,6 +11,7 @@ module Ema.Route.Encoder
|
|||||||
singletonRouteEncoderFrom,
|
singletonRouteEncoderFrom,
|
||||||
isoRouteEncoder,
|
isoRouteEncoder,
|
||||||
showReadRouteEncoder,
|
showReadRouteEncoder,
|
||||||
|
stringRouteEncoder,
|
||||||
mapRouteEncoder,
|
mapRouteEncoder,
|
||||||
leftRouteEncoder,
|
leftRouteEncoder,
|
||||||
rightRouteEncoder,
|
rightRouteEncoder,
|
||||||
@ -177,12 +178,11 @@ isoRouteEncoder iso =
|
|||||||
|
|
||||||
showReadRouteEncoder :: (Show r, Read r) => RouteEncoder () r
|
showReadRouteEncoder :: (Show r, Read r) => RouteEncoder () r
|
||||||
showReadRouteEncoder =
|
showReadRouteEncoder =
|
||||||
unsafeMkRouteEncoder (const enc) (const dec) (const [])
|
isoRouteEncoder $ iso ((<> ".html") . show) (readMaybe <=< fmap toString . T.stripSuffix ".html" . toText)
|
||||||
where
|
|
||||||
enc r = show r <> ".html"
|
stringRouteEncoder :: (IsString a, ToString a) => RouteEncoder () a
|
||||||
dec fp = do
|
stringRouteEncoder =
|
||||||
x' <- fmap toString $ T.stripSuffix ".html" $ toText fp
|
isoRouteEncoder $ iso ((<> ".html") . toString) (fmap (fromString . toString) . T.stripSuffix ".html" . toText)
|
||||||
readMaybe x'
|
|
||||||
|
|
||||||
-- | Route encoder for single route encoding to 'index.html'
|
-- | Route encoder for single route encoding to 'index.html'
|
||||||
singletonRouteEncoder :: RouteEncoder a ()
|
singletonRouteEncoder :: RouteEncoder a ()
|
||||||
|
@ -13,7 +13,6 @@ import Data.LVar qualified as LVar
|
|||||||
import Data.Text qualified as T
|
import Data.Text qualified as T
|
||||||
import Ema.Asset
|
import Ema.Asset
|
||||||
import Ema.CLI
|
import Ema.CLI
|
||||||
import Ema.Model
|
|
||||||
import Ema.Route.Class
|
import Ema.Route.Class
|
||||||
import Ema.Route.Encoder
|
import Ema.Route.Encoder
|
||||||
( checkRouteEncoderForSingleRoute,
|
( checkRouteEncoderForSingleRoute,
|
||||||
|
Loading…
Reference in New Issue
Block a user