{-# language DataKinds #-} {-# language GADTs #-} {-# language KindSignatures #-} {-# language OverloadedStrings #-} {-# language ScopedTypeVariables #-} {-# language TypeApplications #-} {-# language TypeOperators #-} {-# language TypeFamilies #-} {-# language PolyKinds #-} module Main where import Data.ByteString.Lazy (ByteString) import Data.Functor.Identity import Data.Kind (Type) import Data.Text (Text) import Ditto (Result(..)) import Ditto.Core import Ditto.Lucid import Ditto.Lucid.Named import Lucid import Network.Wai (Application) import Network.Wai.Handler.Warp (run) import Network.Wai.Middleware.RequestLogger (logStdoutDev) import Trasa.Core import Trasa.Extra import Trasa.Form import Trasa.Form.Lucid import Trasa.Server import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL import qualified Data.Text as T import qualified Data.Text.Encoding as TE import qualified Data.Text.Read as TR import qualified Text.Read import qualified Trasa.Method as Method tshow :: Show a => a -> Text tshow = T.pack . show readInt :: Text -> Either Text Int readInt input = case (TR.signed TR.decimal) input of Left err -> Left $ T.pack err Right (i, _) -> Right i data Foo = Foo Int Bool Int deriving Show -- Our route data type. We define this ourselves. data Route :: [Type] -> [Param] -> Bodiedness -> Type -> Type where HelloWorld :: Route '[ByteString] -- ^ now the path captures the first piece as a ByeString '[('Optional ByteString)] -- ^ there is an optional query parameter, decoded as a ByteString 'Bodyless -- ^ the route does not have a request body ByteString -- ^ the response body will be `ByteString` FormTest :: Route '[] '[] 'Bodyless (Html ()) FormTestPost :: Route '[] '[] ('Body B.ByteString) (Html ()) bodyAny :: BodyCodec B.ByteString bodyAny = BodyCodec (pure "*/*") (BL.fromStrict) (Right . BL.toStrict) bodyText :: BodyCodec ByteString bodyText = BodyCodec (pure "text/html; charset=utf-8") -- ^ NonEmpty list of the HTTP media type names. id -- ^ encode from ByteString to ByteString Right -- ^ decode from ByteString to (Either Text ByteString) bodyHtml :: BodyCodec (Html a) bodyHtml = BodyCodec (pure "text/html;charset=utf-8") renderBS (const (Left "can not decode html")) bytestring :: CaptureCodec ByteString bytestring = CaptureCodec (TE.decodeUtf8 . BL.toStrict) (Just . BL.fromStrict . TE.encodeUtf8) text :: CaptureCodec Text text = CaptureCodec id Just showReadCodec :: Show a => Read a => CaptureCodec a showReadCodec = CaptureCodec tshow (Text.Read.readMaybe . T.unpack) int :: CaptureCodec Int int = showReadCodec -- | metadata about our routes: value level functions and data for constructing -- and decoding paths meta :: Route captures queries request response -> MetaCodec captures queries request response meta route = case route of HelloWorld -> Meta (capture bytestring ./ end) -- ^ match "/hello" (optional "b" bytestring .& qend) -- ^ no query parameters bodyless -- ^ no request body (resp (one bodyText)) -- ^ response body is one BodyCodec: our bodyText function above Method.get -- ^ http method: GET FormTest -> Meta (match "test" ./ end) (qend) bodyless (resp (one bodyHtml)) Method.get FormTestPost -> Meta (match "test" ./ match "post" ./ end) (qend) (body (one bodyAny)) (resp (one bodyHtml)) Method.post -- | this function defines how we handle routes with our web server: -- what actions we perform based on the route and its captures & queries routes :: forall captures queries request response. Route captures queries request response -- ^ our route GADT, polymorphic over its type variables -> Rec Identity captures -- ^ an extensible record of the captures for this route -> Rec Parameter queries -- ^ an extensible record of the captures for this route -> RequestBody Identity request -- ^ the request body -> TrasaT IO response -- ^ our response routes route captures queries reqBody = case route of HelloWorld -> go helloWorld FormTest -> go formTest FormTestPost -> go formTestPost where -- | this helper function uses the `handler` function to unwrap the `Arguments` type family. go :: Arguments captures queries request (TrasaT IO response) -> TrasaT IO response go f = handler captures queries reqBody f helloWorld :: ByteString -> Maybe ByteString -> TrasaT IO ByteString helloWorld a (Just b) = pure $ a <> ", " <> b <> "!" helloWorld a Nothing = pure a type family QueryArguments (querys :: [Param]) (result :: Type) :: Type where QueryArguments '[] r = r QueryArguments (q ': qs) r = ParamBase q -> QueryArguments qs r -- formArgs :: Monad f => Rec Parameter qrys -> QueryArguments qrys (f (Rec Parameter qrys)) -- formArgs queries args = do -- pure RecNil formFoo :: TrasaSimpleForm Foo formFoo = childErrorList ++> ( Foo <$> label "Int Field 1" "int1" ++> setAttr [class_ "input"] (inputInt readInt "int1" 0) <*> label "Bool Field 1" "bool1" ++> inputYesNo "bool1" <*> label "Int Field 2" "in2" ++> inputInt readInt "int2" 0 <* buttonSubmit (const (Right T.empty)) "" "" ("Submit" :: Text) ) prepare :: Route captures query request response -> Arguments captures query request (Prepared Route response) prepare = prepareWith meta instance IsRoute Route where metaF = meta formTest :: TrasaT IO (Html ()) formTest = do (res, html) <- simpleReformGET (encodeRoute $ conceal (prepare FormTest)) formFoo defaultLayout $ do case res of Ok x -> do toHtml $ show x br_ [] Error xs -> do toHtml $ show xs br_ [] html formTestPost :: B.ByteString -> TrasaT IO (Html ()) formTestPost _ = do pure $ pure () defaultLayout :: Html () -> TrasaT IO (Html ()) defaultLayout children = do pure $ html_ $ do head_ $ do link_ [rel_ "stylesheet", href_ "https://unpkg.com/sakura.css/css/sakura.css", type_ "text/css"] body_ $ do children -- | We define a list of all the routes for our server for wai allRoutes :: [Constructed Route] allRoutes = [Constructed HelloWorld, Constructed FormTest, Constructed FormTestPost] -- | Another implimentaiton detail: this creates the data structure used to do routing router :: Router Route router = routerWith (mapMeta captureDecoding captureDecoding id id . meta) allRoutes -- | `wai` application application :: Application application = serveWith (metaCodecToMetaServer . meta) -- ^ implimentaiton detail: this just marshals some types routes -- ^ routes function defined above router -- ^ router function defined above main :: IO () main = run 8080 (logStdoutDev application) inputYesNo :: String -> TrasaSimpleForm Bool inputYesNo s = mapView (\x -> label_ [for_ (T.pack s)] $ x *> "Enabled") (inputCheckbox False s)