on master help sign in
frontend pretty much complete
commita5a5804da3fd91e1d27c556470bab9c9df34855d authorOwen Shepherd <414owen@gmail.com> dateTue 17 Dec 2019, 01:02 (7 years ago) parent24c0f64
17 files changed+191−38
examples/url-shortener/.gitignore+1 examples/url-shortener/.obelisk/impl/github.json+3−3 examples/url-shortener/backend/backend.cabal+2 examples/url-shortener/backend/src/Backend.hs+16−7 examples/url-shortener/common/common.cabal+2 examples/url-shortener/common/src/Common/Api.hs+13−2 examples/url-shortener/common/src/Common/Route.hs+26−16 examples/url-shortener/common/src/Common/Schema.hs+11 examples/url-shortener/default.nix+13−1 examples/url-shortener/dep/aeson-gadt-th/default.nix+7 examples/url-shortener/dep/aeson-gadt-th/github.json+7 examples/url-shortener/dep/database-id/default.nix+7 examples/url-shortener/dep/database-id/github.json+7 examples/url-shortener/dep/gargoyle/default.nix+7 examples/url-shortener/dep/gargoyle/github.json+7 examples/url-shortener/frontend/src-bin/main.hs+1−1 examples/url-shortener/frontend/src/Frontend.hs+61−8
examples/url-shortener/.gitignore+1
@@ -1,3 +1,4 @@
1+db
12dist-newstyle
23result
34result-android
examples/url-shortener/.obelisk/impl/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "obsidiansystems",
33 "repo": "obelisk",
4− "branch": "master",
5− "rev": "a98ad63b88846412f73a43367e496fa58f77c785",
6− "sha256": "0k5d6687s64ml2yg21c8bjnddgk86fcaq0k25cs41ip5x396akdj"
4+ "branch": "develop",
5+ "rev": "acc5e6db09462d76c7e7eaff3aa3634a7ce0c371",
6+ "sha256": "1q14gmn9152sc7v2pc1wblhyhy2hj5rn7nvrwj5hx8xkmxj68r20"
77}
examples/url-shortener/backend/backend.cabal+2
@@ -12,6 +12,8 @@ library
1212 , frontend
1313 , obelisk-backend
1414 , obelisk-route
15+ , gargoyle-postgresql-connect
16+ , resource-pool
1517 exposed-modules:
1618 Backend
1719 ghc-options: -Wall
examples/url-shortener/backend/src/Backend.hs+16−7
@@ -6,20 +6,29 @@
66{-# LANGUAGE TypeFamilies #-}
77module Backend where
88
9+import Prelude hiding (id)
910import Common.Route
1011import Obelisk.Backend
1112import Obelisk.Route
1213import Snap.Core
14+import Gargoyle.PostgreSQL.Connect
1315
1416backend :: Backend BackendRoute FrontendRoute
1517backend = Backend
1618 { _backend_run = \serve -> do
17− serve $ \case
18− BackendRoute_Missing :/ () -> do
19− modifyResponse $ setResponseStatus 400 "Malformed Request"
20− writeBS "400 Malformed Request"
21− r <- getResponse
22− finishWith r
19+ withDb "db" $ \pool ->
20+ serve $ \case
21+ BackendRoute_Missing :/ () -> do
22+ modifyResponse $ setResponseStatus 400 "Malformed Request"
23+ writeBS "400 Malformed Request"
24+ r <- getResponse
25+ finishWith r
26+ BackendRoute_Shorten :/ () -> do
27+ modifyResponse $ setResponseStatus 200 "OK"
28+ writeBS $ "\"OK\""
29+ r <- getResponse
30+ finishWith r
31+ _ -> error "no"
2332 return ()
24− , _backend_routeEncoder = backendRouteEncoder
33+ , _backend_routeEncoder = fullRouteEncoder
2534 }
examples/url-shortener/common/common.cabal+2
@@ -6,6 +6,8 @@ build-type: Simple
66library
77 hs-source-dirs: src
88 build-depends: base
9+ , aeson-gadt-th
10+ , database-id-obelisk
911 , obelisk-route
1012 , mtl
1113 , text
examples/url-shortener/common/src/Common/Api.hs+13−2
@@ -1,4 +1,15 @@
1+{-# LANGUAGE DeriveGeneric #-}
2+{-# language DeriveAnyClass #-}
3+{-# LANGUAGE ConstraintKinds #-}
4+{-# LANGUAGE GADTs #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
19module Common.Api where
210
3−commonStuff :: String
4−commonStuff = "Here is a string defined in Common.Api"
11+import Data.Aeson
12+import Data.Text (Text)
13+import GHC.Generics (Generic)
14+
15+data ShortenRequest = ShortenRequest Text deriving (Generic, ToJSON, FromJSON)
examples/url-shortener/common/src/Common/Route.hs+26−16
@@ -18,32 +18,42 @@ import Control.Category
1818-}
1919
2020import Data.Text (Text)
21+import qualified Data.Text as T
22+import Data.Function
2123import Data.Functor.Identity
22−import Data.Functor.Sum
24+import Database.Id.Class (Id)
25+import Database.Id.Obelisk.Route (idPathSegmentEncoder)
2326
2427import Obelisk.Route
2528import Obelisk.Route.TH
2629
30+import Common.Schema
31+
2732data BackendRoute :: * -> * where
28− -- | Used to handle unparseable routes.
2933 BackendRoute_Missing :: BackendRoute ()
30− -- You can define any routes that will be handled specially by the backend here.
31− -- i.e. These do not serve the frontend, but do something different, such as serving static files.
34+ BackendRoute_Url_Id :: BackendRoute (Id Url)
35+ -- TODO validate this or use a URL type
36+ BackendRoute_Shorten :: BackendRoute ()
3237
3338data FrontendRoute :: * -> * where
3439 FrontendRoute_Main :: FrontendRoute ()
35− -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
36−
37−backendRouteEncoder
38− :: Encoder (Either Text) Identity (R (Sum BackendRoute (ObeliskRoute FrontendRoute))) PageName
39−backendRouteEncoder = handleEncoder (const (InL BackendRoute_Missing :/ ())) $
40− pathComponentEncoder $ \case
41− InL backendRoute -> case backendRoute of
42− BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty
43− InR obeliskRoute -> obeliskRouteSegment obeliskRoute $ \case
44− -- The encoder given to PathEnd determines how to parse query parameters,
45− -- in this example, we have none, so we insist on it.
46− FrontendRoute_Main -> PathEnd $ unitEncoder mempty
40+
41+checkedFullRouteEncoder
42+ :: Encoder Identity Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
43+checkedFullRouteEncoder = checkEncoder fullRouteEncoder & \case
44+ Left err -> error $ T.unpack err
45+ Right encoder -> encoder
46+
47+fullRouteEncoder
48+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
49+fullRouteEncoder = mkFullRouteEncoder
50+ (FullRoute_Backend BackendRoute_Missing :/ ())
51+ (\case
52+ BackendRoute_Shorten -> PathSegment "create" $ unitEncoder mempty
53+ BackendRoute_Url_Id -> PathSegment "s" idPathSegmentEncoder
54+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
55+ (\case
56+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
4757
4858concat <$> mapM deriveRouteComponent
4959 [ ''BackendRoute
examples/url-shortener/common/src/Common/Schema.hsnew file+11
@@ -0,0 +1,11 @@
1+{-# LANGUAGE DeriveGeneric #-}
2+module Common.Schema where
3+
4+import Data.Text (Text)
5+import Database.Id.Class
6+import GHC.Generics
7+
8+data Url = Url { unUrl :: Text }
9+ deriving Generic
10+
11+instance HasId Url
examples/url-shortener/default.nix+13−1
@@ -7,8 +7,20 @@
77 # config.android_sdk.accept_license = false;
88 }
99}:
10−with obelisk;
10+let deps = obelisk.nixpkgs.thunkSet ./dep;
11+ haskellLib = obelisk.nixpkgs.haskell.lib;
12+in with obelisk;
1113project ./. ({ ... }: {
14+ packages = {
15+ database-id-class = deps.database-id + /class;
16+ database-id-groundhog = deps.database-id + /groundhog;
17+ database-id-obelisk = deps.database-id + /obelisk;
18+ };
19+ overrides = self: super: import dep/gargoyle self // {
20+ aeson-gadt-th = (self.callCabal2nix "aeson-gadt-th" deps.aeson-gadt-th {}).overrideAttrs (drv: {
21+ configureFlags = drv.configureFlags or [] ++ ["-f-build-readme"]; # Upstream issue: readme doesn't build on ios.
22+ });
23+ };
1224 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
1325 android.displayName = "Obelisk Minimal Example";
1426 ios.bundleIdentifier = "systems.obsidian.obelisk.examples.minimal";
examples/url-shortener/dep/aeson-gadt-th/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
examples/url-shortener/dep/aeson-gadt-th/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "obsidiansystems",
3+ "repo": "aeson-gadt-th",
4+ "branch": "develop",
5+ "rev": "65df0b070dd21e4c2285fc485e5b9857dbc8ad5b",
6+ "sha256": "1h6wkag38r9xmf3yd0f64h0mfzb7cfj8cf7n2r5mqdyiqz3a5x4w"
7+}
examples/url-shortener/dep/database-id/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
examples/url-shortener/dep/database-id/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "obsidiansystems",
3+ "repo": "database-id",
4+ "branch": "develop",
5+ "rev": "216c23e6e19bfe3330ecba553b551f357bffe119",
6+ "sha256": "17k8dc8j4bddfmlh60d9w6015vb2bi6w0a9sr8kr36hidzrgdy5q"
7+}
examples/url-shortener/dep/gargoyle/default.nixnew file+7
@@ -0,0 +1,7 @@
1+# DO NOT HAND-EDIT THIS FILE
2+import ((import <nixpkgs> {}).fetchFromGitHub (
3+ let json = builtins.fromJSON (builtins.readFile ./github.json);
4+ in { inherit (json) owner repo rev sha256;
5+ private = json.private or false;
6+ }
7+))
examples/url-shortener/dep/gargoyle/github.jsonnew file+7
@@ -0,0 +1,7 @@
1+{
2+ "owner": "obsidiansystems",
3+ "repo": "gargoyle",
4+ "branch": "develop",
5+ "rev": "85d1802d0d511a55703b04e25e27f76fb1744001",
6+ "sha256": "1s06x2a4n0c7rr0r0ldq6nwp9m6mls38kczx5z957cyk9wvciva4"
7+}
examples/url-shortener/frontend/src-bin/main.hs+1−1
@@ -6,5 +6,5 @@ import Reflex.Dom
66
77main :: IO ()
88main = do
9− let Right validFullEncoder = checkEncoder backendRouteEncoder
9+ let validFullEncoder = checkedFullRouteEncoder
1010 run $ runFrontend validFullEncoder frontend
examples/url-shortener/frontend/src/Frontend.hs+61−8
@@ -1,29 +1,82 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE FlexibleContexts #-}
13{-# LANGUAGE DataKinds #-}
4+{-# LANGUAGE LambdaCase #-}
25{-# LANGUAGE OverloadedStrings #-}
6+{-# LANGUAGE RecursiveDo #-}
37{-# LANGUAGE TemplateHaskell #-}
48{-# LANGUAGE TypeApplications #-}
59module Frontend where
610
7−import qualified Data.Text as T
8−import qualified Data.Text.Encoding as T
11+import Data.Text (Text)
912import Obelisk.Frontend
10−import Obelisk.Configs
1113import Obelisk.Route
1214import Reflex.Dom.Core
15+import Control.Monad.Fix (MonadFix)
16+import Language.Javascript.JSaddle (MonadJSM)
1317
1418import Common.Api
1519import Common.Route
1620import Obelisk.Generated.Static
1721
22+type AppWidget js t m =
23+ ( DomBuilder t m
24+ , MonadFix m
25+ , MonadHold t m
26+ , TriggerEvent t m
27+ , PostBuild t m
28+ , PerformEvent t m
29+ , Prerender js t m
30+ )
31+
32+type FrontendWidget js t m =
33+ ( AppWidget js t m
34+ , MonadJSM m
35+ , MonadJSM (Performable m)
36+ , HasJSContext (Performable m)
37+ )
38+
39+data State = NotStarted | Loading | Loaded (Maybe Text)
40+
41+urlInput :: AppWidget js t m => m (Dynamic t State)
42+urlInput = do
43+ inputEl <- el "label" $ do
44+ text "url: "
45+ inputElement def
46+ click <- button "shorten"
47+ let url = tagPromptlyDyn (_inputElement_value inputEl) click
48+ request <- prerender
49+ (pure never)
50+ ((decodeXhrResponse <$>) <$> (performRequestAsync $ shortenRequest <$> url))
51+ let requestEv = switchDyn request
52+ holdDyn NotStarted $
53+ leftmost [Loading <$ click, Loaded <$> requestEv]
54+
55+createShortLinkRoute :: Text
56+createShortLinkRoute = renderBackendRoute checkedFullRouteEncoder $ BackendRoute_Shorten :/ ()
57+
58+shortenRequest :: Text -> XhrRequest Text
59+shortenRequest s = postJson createShortLinkRoute (ShortenRequest s)
60+
61+createdLink :: AppWidget js t m => State -> m ()
62+createdLink = \case
63+ NotStarted -> blank
64+ Loading -> text "loading"
65+ Loaded a -> case a of
66+ Nothing -> text "Error"
67+ Just url -> text url
68+
69+app :: AppWidget js t m => m ()
70+app = do
71+ state <- urlInput
72+ _ <- dyn $ createdLink <$> state
73+ blank
74+
1875frontend :: Frontend (R FrontendRoute)
1976frontend = Frontend
2077 { _frontend_head = do
2178 el "title" $ text "Url Shortener"
2279 elAttr "link" ("rel" =: "stylesheet" <> "href" =: static @"style.css") blank
2380 , _frontend_body = do
24− url <- el "label" $ do
25− text "url: "
26− inputElement def
27− shorten <- button "shorten"
28− return ()
81+ app
2982 }