on master help sign in
Merge branch 'develop'
commita98ad63b88846412f73a43367e496fa58f77c785 authorRyan Trinkle <ryan.trinkle@gmail.com> dateSat 17 Aug 2019, 22:24 (7 years ago) parentde99fc6e5bd022
47 files changed+706−510
.gitignore+1−3 CONTRIBUTING.md+13 ChangeLog.md+9 README.md+5 default.nix+4−10 dep/reflex-platform/github.json+3−3 lib/backend/obelisk-backend.cabal+1−1 lib/backend/src/Obelisk/Backend.hs+22−37 lib/command/src/Obelisk/Command/Run.hs+4−1 lib/command/src/Obelisk/Command/VmBuilder.hs+2−1 lib/executable-config/backend/obelisk-executable-config-backend.cabal−20 lib/executable-config/backend/src/Obelisk/ExecutableConfig/Backend.hs−77 lib/executable-config/default.nix+1−3 lib/executable-config/frontend/obelisk-executable-config-frontend.cabal−21 lib/executable-config/frontend/src/Obelisk/ExecutableConfig/Frontend.hs−115 lib/executable-config/inject/obelisk-executable-config-inject.cabal+2 lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+8−5 lib/executable-config/lookup/default.nix+4−2 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+16−8 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hs+11−7 lib/executable-config/lookup/src-directory/Obelisk/ExecutableConfig/Internal/ConfigDirectory.hs−24 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+25−10 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hs+5−3 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+18 lib/executable-config/lookup/src/Obelisk/Configs.hs+124 lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hs+24 lib/executable-config/lookup/src/Obelisk/ExecutableConfig/Lookup.hs−9 lib/frontend/obelisk-frontend.cabal−1 lib/frontend/src/Obelisk/Frontend.hs+120−38 lib/frontend/src/Obelisk/Frontend/Cookie.hs+3−3 lib/route/obelisk-route.cabal+7−3 lib/route/src/Obelisk/Route.hs+107−32 lib/route/src/Obelisk/Route/Frontend.hs+24−24 lib/route/src/Obelisk/Route/TH.hs+4−5 lib/run/obelisk-run.cabal+1−1 lib/run/src/Obelisk/Run.hs+27−30 lib/selftest/src/Obelisk/SelfTest.hs+12−10 lib/tabulation/CHANGELOG.md+5 lib/tabulation/LICENSE+30 lib/tabulation/Setup.hs+2 lib/tabulation/src/Data/Tabulation.hs+21 lib/tabulation/tabulation.cabal+26 release.nix+4−1 skeleton/common/src/Common/Api.hs+1−1 skeleton/config/common/example+1 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src/Frontend.hs+8−1
.gitignore+1−3
@@ -4,12 +4,10 @@
44result
55*.o
66*.hi
7−
87TAGS
98tags
109ctags
1110dist-newstyle
1211cabal.project.local
1312.ghc.environment.*
14−
15−**/ghcid-output.txt
13+**/ghcid-output.txt
CONTRIBUTING.md+13
@@ -5,6 +5,7 @@ Contributions and issue reports are encouraged and appreciated!
55- [Opening Issues](#opening-issues)
66- [Submitting Changes](#submitting-changes)
77 - [Guidelines for Commit Messages](#guidelines-for-commit-messages)
8+ - [Guidelines for Pull Requests](#guidelines-for-pull-requests)
89 - [Code Quality](#code-quality)
910 - [Documentation](#documentation)
1011
@@ -34,6 +35,10 @@ Commits that update a dependency should include some information about why the d
3435#### Body
3536For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
3637
38+### Guidelines for Pull Requests
39+
40+Wherever possible, pull requests should add a single feature or fix a single bug. Pull requests should not bundle several unrelated changes.
41+
3742### Code Quality
3843
3944#### Warnings
@@ -49,6 +54,14 @@ You can run the tests like this:
4954$(nix-build -A selftest --no-out-link)
5055```
5156
57+To test that your changes build across platforms, you can also try to build release.nix, like this:
58+```bash
59+nix-build release.nix
60+```
61+
62+Note, however, that to build release.nix you must accept the android license agreement and your machine must be configured to build both ios and android executables (usually via remote builders).
63+
64+
5265### Documentation
5366
5467#### In the code
ChangeLog.md+9
@@ -4,6 +4,15 @@ This project's release branch is `master`. This log is written from the perspect
44
55## Unreleased
66
7+## v0.2.0.0 - 2019-8-17
8+
9+* Configs become ByteStrings.
10+* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
11+ * The frontend will still only have access to configs that are placed in config/frontend and config/common, while the backend has access to the entire contents of the config directory via `Obelisk.ExecutableConfig.Lookup.getConfigs`.
12+* The backend no longer runs in BackendConfigsT.
13+* Add tabulation package. See Data.Tabulation for details.
14+* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
15+* Use IP address for nginx proxy pass instead of localhost
716
817## v0.1.1.0 - 2019-05-17
918
README.md+5
@@ -5,6 +5,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
55- [Installing Obelisk](#installing-obelisk)
66- [Developing an Obelisk project](#developing-an-obelisk-project)
77 - [Hoogle](#hoogle)
8+ - [Adding Packages](#adding-packages)
89 - [Adding Package Overrides](#adding-package-overrides)
910- [Deploying](#deploying)
1011 - [Locally](#locally)
@@ -92,6 +93,10 @@ To enter a nix-shell from which you can run the hoogle command-line client or a
9293
9394`nix-shell -A shells.ghc --arg withHoogle true`
9495
96+### Adding packages
97+
98+In order to add package dependencies, declare them under the build-depends field in the appropriate cabal files (backend, common, and frontend each have their own). The corresponding Nix packages will automatically be selected when building.
99+
95100### Adding package overrides
96101
97102To add a version override to any Haskell package, or to add a Haskell package that doesn't exist in the nixpkgs used by Obelisk, use the `overrides` attribute in your project's `default.nix`. For example, to use a specific version of the `aeson` package, your `default.nix` will look like:
default.nix+4−10
@@ -39,14 +39,6 @@ let
3939 rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
4040 sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
4141 }) {});
42− # Need 8.0.2 build support
43− # PR: https://github.com/dmwit/universe/pull/33
44− universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
45− owner = "obsidiansystems";
46− repo = "universe";
47− rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
48− sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
49− } + /template) {};
5042 })
5143
5244 pkgs.obeliskExecutableConfig.haskellOverlay
@@ -67,6 +59,7 @@ let
6759 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
6860 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
6961 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
62+ tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
7063 })
7164
7265 (self: super: let
@@ -205,7 +198,7 @@ in rec {
205198 enableACME = enableHttps;
206199 forceSSL = enableHttps;
207200 locations.${baseUrl} = {
208− proxyPass = "http://localhost:" + toString internalPort;
201+ proxyPass = "http://127.0.0.1:" + toString internalPort;
209202 proxyWebsockets = true;
210203 };
211204 };
@@ -214,10 +207,11 @@ in rec {
214207 wantedBy = [ "multi-user.target" ];
215208 after = [ "network.target" ];
216209 restartIfChanged = true;
210+ path = [ pkgs.gnutar ];
217211 script = ''
218212 ln -sft . '${exe}'/*
219213 mkdir -p log
220− exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
214+ exec ./backend ${backendArgs} </dev/null
221215 '';
222216 serviceConfig = {
223217 User = user;
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
5− "rev": "d68ab80e3a92425d4bcbfa2da7c685aded550c16",
6− "sha256": "0d5hc4v56pldy65rl0pmdzx2jylh8rjqqg5n0qb48039yy3pp8cq"
4+ "branch": "cg-bump-reflex-dom",
5+ "rev": "7ecb4c255bb053a7a4796ffd6331cdbe210de91c",
6+ "sha256": "1mngxa24cfpvxxq4hgh77nw36vny4abl5wi2xmlwpkk25wzm0h0x"
77}
lib/backend/obelisk-backend.cabal+1−1
@@ -16,7 +16,7 @@ library
1616 lens,
1717 mtl,
1818 obelisk-asset-serve-snap,
19− obelisk-executable-config-backend,
19+ obelisk-executable-config-inject,
2020 obelisk-executable-config-lookup,
2121 obelisk-frontend,
2222 obelisk-route,
lib/backend/src/Obelisk/Backend.hs+22−37
@@ -11,7 +11,6 @@ module Obelisk.Backend
1111 ( Backend (..)
1212 -- * Re-exports
1313 , Default (def)
14− , ComponentConfigs(..)
1514 , getPageName
1615 , getRouteWith
1716 , runSnapWithCommandLineArgs
@@ -20,7 +19,7 @@ module Obelisk.Backend
2019 , runBackend
2120 , staticRenderContentType
2221 , mkRouteToUrl
23− , getComponentConfigs
22+ , getPublicConfigs
2423 ) where
2524
2625import Prelude hiding (id, (.))
@@ -41,8 +40,7 @@ import Data.Text (Text)
4140import qualified Data.Text as T
4241import Data.Text.Encoding
4342import Obelisk.Asset.Serve.Snap (serveAsset)
44−import Obelisk.ExecutableConfig.Backend
45−import Obelisk.ExecutableConfig.Lookup
43+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4644import Obelisk.Frontend
4745import Obelisk.Route
4846import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
@@ -55,14 +53,7 @@ import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
5553
5654data Backend backendRoute frontendRoute = Backend
5755 { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
58− , _backend_run :: ((R backendRoute -> BackendConfigsT Snap ()) -> BackendConfigsT IO ()) -> BackendConfigsT IO ()
59− }
60−
61−-- | All the configs separated by which context they are accessible in. (Common
62−-- configs are present in both.)
63−data ComponentConfigs = ComponentConfigs
64− { _componentConfigs_backend :: Map Text Text
65− , _componentConfigs_frontend :: Map Text Text
56+ , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
6657 }
6758
6859-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
@@ -80,7 +71,7 @@ serveDefaultObeliskApp
8071 -> ([Text]
8172 -> m ())
8273 -> Frontend (R appRoute)
83− -> Map Text Text
74+ -> Map Text ByteString
8475 -> R (ObeliskRoute appRoute)
8576 -> m ()
8677serveDefaultObeliskApp urlEnc serveStaticAsset frontend configs = serveObeliskApp urlEnc serveStaticAsset frontendApp configs
@@ -139,7 +130,7 @@ serveObeliskApp
139130 => (R appRoute -> Text)
140131 -> ([Text] -> m ())
141132 -> GhcjsApp (R appRoute)
142− -> Map Text Text
133+ -> Map Text ByteString
143134 -> R (ObeliskRoute appRoute)
144135 -> m ()
145136serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
@@ -174,7 +165,7 @@ serveGhcjsApp
174165 :: (MonadSnap m, HasCookies m)
175166 => (R appRouteComponent -> Text)
176167 -> GhcjsApp (R appRouteComponent)
177− -> Map Text Text
168+ -> Map Text ByteString
178169 -> R (GhcjsAppRoute appRouteComponent)
179170 -> m ()
180171serveGhcjsApp urlEnc app config = \case
@@ -187,17 +178,15 @@ runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> I
187178runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
188179 Left e -> fail $ "backend error:\n" <> T.unpack e
189180 Right validFullEncoder -> do
190− configs <- getComponentConfigs
191− runBackendConfigsT (_componentConfigs_backend configs) $
192− _backend_run backend $ \serveRoute -> do
193− runSnapWithCommandLineArgs $ do
194− getRouteWith validFullEncoder >>= \case
195− Identity r -> case r of
196− InL backendRoute :=> Identity a ->
197− runBackendConfigsT (_componentConfigs_backend configs) $
198− serveRoute $ backendRoute :/ a
199− InR obeliskRoute :=> Identity a ->
200− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend (_componentConfigs_frontend configs) $ obeliskRoute :/ a
181+ publicConfigs <- getPublicConfigs
182+ _backend_run backend $ \serveRoute -> do
183+ runSnapWithCommandLineArgs $ do
184+ getRouteWith validFullEncoder >>= \case
185+ Identity r -> case r of
186+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
187+ InR obeliskRoute :=> Identity a ->
188+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
189+ obeliskRoute :/ a
201190
202191mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
203192mkRouteToUrl validFullEncoder =
@@ -208,7 +197,7 @@ renderGhcjsFrontend
208197 :: (MonadSnap m, HasCookies m)
209198 => (route -> Text)
210199 -> route
211− -> Map Text Text
200+ -> Map Text ByteString
212201 -> Frontend route
213202 -> m ByteString
214203renderGhcjsFrontend urlEnc route configs f = do
@@ -220,13 +209,9 @@ renderGhcjsFrontend urlEnc route configs f = do
220209instance HasCookies Snap where
221210 askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
222211
223−getComponentConfigs :: IO ComponentConfigs
224−getComponentConfigs = do
225− allConfigs <- getConfigs
226− return $ ComponentConfigs
227− { _componentConfigs_backend =
228− Map.filterWithKey (\k _ -> isMemberOf k ["common", "backend"]) allConfigs
229− , _componentConfigs_frontend =
230− Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) allConfigs
231− }
232− where isMemberOf k dirs = or $ map (\dir -> ("config/" <> dir) `T.isPrefixOf` k) dirs
212+-- | Get configs from the canonical "public" locations (i.e., locations that obelisk expects to make available
213+-- to frontend applications, and hence visible to end users).
214+getPublicConfigs :: IO (Map Text ByteString)
215+getPublicConfigs = Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) <$> Lookup.getConfigs
216+ where
217+ isMemberOf k = any (`T.isPrefixOf` k)
lib/command/src/Obelisk/Command/Run.hs+4−1
@@ -73,7 +73,10 @@ run = do
7373 -- Check whether the impure static files are a derivation (and so must be built)
7474 if isDerivation == "1"
7575 then fmap T.strip $ readProcessAndLogStderr Debug $ -- Strip whitespace here because nix-build has no --raw option
76− proc "nix-build" ["-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"]
76+ proc "nix-build"
77+ [ "--no-out-link"
78+ , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
79+ ]
7780 else readProcessAndLogStderr Debug $
7881 proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
7982 putLog Debug $ "Assets impurely loaded from: " <> assets
lib/command/src/Obelisk/Command/VmBuilder.hs+2−1
@@ -156,7 +156,8 @@ testLinuxBuild stateDir
156156 | System.Info.os == "linux" = failWith "Using the docker builder is not necessary on linux."
157157 | otherwise = do
158158 (exitCode, _stdout, stderr) <- readCreateProcessWithExitCode $ proc "nix-build"
159− [ "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
159+ [ "--no-out-link"
160+ , "-E", "(import <nixpkgs> { system = \"x86_64-linux\"; }).writeText \"test\" builtins.currentTime"
160161 , "--builders", nixBuildersArgString stateDir
161162 ]
162163 unless (exitCode == ExitSuccess) $ do
lib/executable-config/backend/obelisk-executable-config-backend.cabaldeleted−20
@@ -1,20 +0,0 @@
1−name: obelisk-executable-config-backend
2−version: 0.1
3−cabal-version: >= 1.2
4−build-type: Simple
5−
6−library
7− hs-source-dirs: src
8− build-depends: base,
9− containers,
10− monad-control,
11− mtl,
12− primitive,
13− ref-tf,
14− snap-core,
15− text,
16− transformers,
17− transformers-base
18− exposed-modules:
19− Obelisk.ExecutableConfig.Backend
20− ghc-options: -Wall
lib/executable-config/backend/src/Obelisk/ExecutableConfig/Backend.hsdeleted−77
@@ -1,77 +0,0 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE DefaultSignatures #-}
4−{-# LANGUAGE DeriveFunctor #-}
5−{-# LANGUAGE FlexibleContexts #-}
6−{-# LANGUAGE FlexibleInstances #-}
7−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8−{-# LANGUAGE MultiParamTypeClasses #-}
9−{-# LANGUAGE OverloadedStrings #-}
10−{-# LANGUAGE RankNTypes #-}
11−{-# LANGUAGE TypeFamilies #-}
12−{-# LANGUAGE UndecidableInstances #-}
13−module Obelisk.ExecutableConfig.Backend
14− ( HasBackendConfigs(..)
15− , BackendConfigsT
16− , runBackendConfigsT
17− , mapBackendConfigsT
18− ) where
19−
20−import Control.Applicative
21−import Control.Monad
22−import Control.Monad.Base
23−import Control.Monad.Fix
24−import Control.Monad.Primitive
25−import Control.Monad.Ref
26−import Control.Monad.IO.Class
27−import Control.Monad.Trans.Class
28−import Control.Monad.Trans.Control
29−import Control.Monad.Trans.Reader
30−import Data.Map (Map)
31−import qualified Data.Map as Map
32−import Data.Text (Text)
33−import Snap.Core (MonadSnap)
34−
35−class Monad m => HasBackendConfigs m where
36− getBackendConfig :: Text -> m (Maybe Text)
37− default getBackendConfig :: (HasBackendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
38− getBackendConfig = lift . getBackendConfig
39− getCommonConfig :: Text -> m (Maybe Text)
40− default getCommonConfig :: (HasBackendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
41− getCommonConfig = lift . getCommonConfig
42−
43−newtype BackendConfigsT m a = BackendConfigsT { unBackendConfigsT :: ReaderT (Map Text Text) m a }
44− deriving
45− ( Functor
46− , Applicative
47− , Monad
48− , MonadPlus
49− , Alternative
50− , MonadFix
51− , MonadIO
52− , MonadBase m'
53− , MonadBaseControl m'
54− , MonadRef
55− , MonadTrans
56− , MonadSnap
57− )
58−
59−instance PrimMonad m => PrimMonad (BackendConfigsT m) where
60− type PrimState (BackendConfigsT m) = PrimState m
61− primitive = lift . primitive
62−
63−runBackendConfigsT
64− :: Map Text Text
65− -> BackendConfigsT m a
66− -> m a
67−runBackendConfigsT cs child = runReaderT (unBackendConfigsT child) cs
68−
69−instance Monad m => HasBackendConfigs (BackendConfigsT m) where
70− getBackendConfig k = BackendConfigsT $ Map.lookup ("config/backend/" <> k) <$> ask
71− getCommonConfig k = BackendConfigsT $ Map.lookup ("config/common/" <> k) <$> ask
72−
73−mapBackendConfigsT
74− :: (forall x. m x -> n x)
75− -> BackendConfigsT m a
76− -> BackendConfigsT n a
77−mapBackendConfigsT f (BackendConfigsT x) = BackendConfigsT $ mapReaderT f x
lib/executable-config/default.nix+1−3
@@ -16,12 +16,10 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
1616 exit 1
1717 fi
1818 cp -a "${config}"/* "$out/config"
19− (cd $out && find config -type f -print0 >config.files)
19+ find "$out/config" -type f -printf '%P\0' >"$out/config.files"
2020 '');
2121in with nixpkgs.haskell.lib; {
2222 haskellOverlay = self: super: {
23− obelisk-executable-config-backend = self.callCabal2nix "obelisk-executable-config-backend" (filterGitSource ./backend) {};
24− obelisk-executable-config-frontend = self.callCabal2nix "obelisk-executable-config-frontend" (filterGitSource ./frontend) {};
2523 obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
2624 };
2725 platforms = {
lib/executable-config/frontend/obelisk-executable-config-frontend.cabaldeleted−21
@@ -1,21 +0,0 @@
1−name: obelisk-executable-config-frontend
2−version: 0.1
3−cabal-version: >= 1.2
4−build-type: Simple
5−
6−library
7− hs-source-dirs: src
8− build-depends: base,
9− containers,
10− ghcjs-dom,
11− jsaddle,
12− mtl,
13− primitive,
14− ref-tf,
15− reflex,
16− reflex-dom-core,
17− text,
18− transformers
19− exposed-modules:
20− Obelisk.ExecutableConfig.Frontend
21− ghc-options: -Wall
lib/executable-config/frontend/src/Obelisk/ExecutableConfig/Frontend.hsdeleted−115
@@ -1,115 +0,0 @@
1−{-# LANGUAGE CPP #-}
2−{-# LANGUAGE DataKinds #-}
3−{-# LANGUAGE DefaultSignatures #-}
4−{-# LANGUAGE DeriveFunctor #-}
5−{-# LANGUAGE FlexibleContexts #-}
6−{-# LANGUAGE FlexibleInstances #-}
7−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8−{-# LANGUAGE MultiParamTypeClasses #-}
9−{-# LANGUAGE OverloadedStrings #-}
10−{-# LANGUAGE RankNTypes #-}
11−{-# LANGUAGE TypeFamilies #-}
12−{-# LANGUAGE UndecidableInstances #-}
13−module Obelisk.ExecutableConfig.Frontend
14− ( HasFrontendConfigs(..)
15− , FrontendConfigsT
16− , runFrontendConfigsT
17− , mapFrontendConfigsT
18− ) where
19−
20−import Control.Monad.Fix
21−import Control.Monad.Primitive
22−import Control.Monad.Ref
23−import Control.Monad.IO.Class
24−import Control.Monad.Trans.Class
25−import Control.Monad.Trans.Reader
26−import Data.Map (Map)
27−import qualified Data.Map as Map
28−import Data.Text (Text)
29−import Reflex
30−import Reflex.Host.Class
31−import Reflex.Dom.Core
32−#ifndef ghcjs_HOST_OS
33−import GHCJS.DOM.Types (MonadJSM)
34−#endif
35−
36−class Monad m => HasFrontendConfigs m where
37− getFrontendConfig :: Text -> m (Maybe Text)
38− default getFrontendConfig :: (HasFrontendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
39− getFrontendConfig = lift . getFrontendConfig
40− getCommonConfig :: Text -> m (Maybe Text)
41− default getCommonConfig :: (HasFrontendConfigs m', m ~ t m', MonadTrans t) => Text -> m (Maybe Text)
42− getCommonConfig = lift . getCommonConfig
43−
44−instance HasFrontendConfigs m => HasFrontendConfigs (BehaviorWriterT t w m)
45−instance HasFrontendConfigs m => HasFrontendConfigs (DynamicWriterT t w m)
46−instance HasFrontendConfigs m => HasFrontendConfigs (EventWriterT t w m)
47−instance HasFrontendConfigs m => HasFrontendConfigs (PostBuildT t m)
48−instance HasFrontendConfigs m => HasFrontendConfigs (QueryT t q m)
49−instance HasFrontendConfigs m => HasFrontendConfigs (ReaderT r m)
50−instance HasFrontendConfigs m => HasFrontendConfigs (RequesterT t request response m)
51−instance HasFrontendConfigs m => HasFrontendConfigs (StaticDomBuilderT t m)
52−instance HasFrontendConfigs m => HasFrontendConfigs (TriggerEventT t m)
53−
54−newtype FrontendConfigsT m a = FrontendConfigsT { unFrontendConfigsT :: ReaderT (Map Text Text) m a }
55− deriving
56− ( Functor
57− , Applicative
58− , DomBuilder t
59− , Monad
60− , MonadFix
61− , MonadHold t
62− , MonadIO
63−#ifndef ghcjs_HOST_OS
64− , MonadJSM
65−#endif
66− , MonadRef
67− , MonadReflexCreateTrigger t
68− , MonadSample t
69− , MonadTrans
70− , NotReady t
71− , PostBuild t
72− , TriggerEvent t
73− , HasDocument
74− , DomRenderHook t
75− , HasJSContext
76− , HasJS js
77− )
78−
79−instance PerformEvent t m => PerformEvent t (FrontendConfigsT m) where
80− type Performable (FrontendConfigsT m) = FrontendConfigsT (Performable m)
81− performEvent e = FrontendConfigsT $ ReaderT $ \configs ->
82− performEvent $ runFrontendConfigsT configs <$> e
83− performEvent_ e = FrontendConfigsT $ ReaderT $ \configs ->
84− performEvent_ $ runFrontendConfigsT configs <$> e
85−
86−instance Adjustable t m => Adjustable t (FrontendConfigsT m) where
87− runWithReplace a e = FrontendConfigsT $ runWithReplace (unFrontendConfigsT a) (unFrontendConfigsT <$> e)
88− traverseDMapWithKeyWithAdjust f m e = FrontendConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unFrontendConfigsT $ f k v) m e
89− traverseIntMapWithKeyWithAdjust f m e = FrontendConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unFrontendConfigsT $ f k v) m e
90− traverseDMapWithKeyWithAdjustWithMove f m e = FrontendConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unFrontendConfigsT $ f k v) m e
91−
92−instance Prerender js t m => Prerender js t (FrontendConfigsT m) where
93− type Client (FrontendConfigsT m) = FrontendConfigsT (Client m)
94− prerender server client = FrontendConfigsT $ ReaderT $ \configs ->
95− prerender (runFrontendConfigsT configs server) (runFrontendConfigsT configs client)
96−
97−instance PrimMonad m => PrimMonad (FrontendConfigsT m) where
98− type PrimState (FrontendConfigsT m) = PrimState m
99− primitive = lift . primitive
100−
101−runFrontendConfigsT
102− :: Map Text Text
103− -> FrontendConfigsT m a
104− -> m a
105−runFrontendConfigsT cs child = runReaderT (unFrontendConfigsT child) cs
106−
107−instance Monad m => HasFrontendConfigs (FrontendConfigsT m) where
108− getFrontendConfig k = FrontendConfigsT $ Map.lookup ("config/frontend/" <> k) <$> ask
109− getCommonConfig k = FrontendConfigsT $ Map.lookup ("config/common/" <> k) <$> ask
110−
111−mapFrontendConfigsT
112− :: (forall x. m x -> n x)
113− -> FrontendConfigsT m a
114− -> FrontendConfigsT n a
115−mapFrontendConfigsT f (FrontendConfigsT x) = FrontendConfigsT $ mapReaderT f x
lib/executable-config/inject/obelisk-executable-config-inject.cabal+2
@@ -8,6 +8,8 @@ library
88
99 build-depends:
1010 base,
11+ bytestring,
12+ base64-bytestring,
1113 containers,
1214 reflex-dom-core,
1315 text
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+8−5
@@ -3,20 +3,23 @@ module Obelisk.ExecutableConfig.Inject where
33
44import Control.Monad (mapM_)
55import Control.Monad.IO.Class (MonadIO)
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString.Base64 as B64
68import Data.Map (Map)
79import qualified Data.Map as Map
810import Data.Semigroup ((<>))
911import Data.Text (Text)
12+import qualified Data.Text.Encoding as T
1013import Reflex.Dom.Core hiding (value)
1114
1215-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
13−-- whose value is the provided configuration
14−injectPure :: DomBuilder t m => Text -> Text -> m ()
16+-- whose value is the provided configuration, encoded in base64.
17+injectPure :: DomBuilder t m => Text -> ByteString -> m ()
1518injectPure key value =
16− let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key)
17− in elAttr "script" attrs $ text value
19+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key <> "data-hydration-skip" =: "")
20+ in elAttr "script" attrs $ text (T.decodeUtf8 (B64.encode value))
1821
1922-- | Produces injectable @<script>@ tags containing the configuration keys
2023-- (filepaths) and values.
21−injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text Text -> m ()
24+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text ByteString -> m ()
2225injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
lib/executable-config/lookup/default.nix+4−2
@@ -1,5 +1,7 @@
11{ hostPlatform, ghc
2−, mkDerivation, base, bytestring, filepath, stdenv, text, transformers
2+, mkDerivation, base, base64-bytestring, bytestring, filepath, stdenv, text, transformers
3+, transformers-base, ref-tf, primitive, monad-control, reflex, reflex-dom
4+, jsaddle
35, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
46}:
57let isAndroid = hostPlatform.libc == "bionic";
@@ -10,7 +12,7 @@ in mkDerivation {
1012 version = "0.1";
1113 src = ./.;
1214 libraryHaskellDepends = [
13− base bytestring filepath text transformers
15+ base bytestring filepath text transformers transformers-base ref-tf primitive monad-control reflex reflex-dom jsaddle
1416 ] ++ (if isAndroid then [
1517 android-activity
1618 ] else if isIOS then [
lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+16−8
@@ -4,34 +4,42 @@ cabal-version: >= 1.2
44build-type: Simple
55
66library
7− exposed-modules: Obelisk.ExecutableConfig.Lookup
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup, Obelisk.Configs
88 if os(ios)
9− hs-source-dirs: src-ios, src-directory
10− other-modules: Obelisk.ExecutableConfig.Internal.ConfigDirectory
9+ hs-source-dirs: src-ios, src
10+ other-modules: Obelisk.Configs.Internal.Directory
1111 build-depends: jsaddle-wkwebview
1212 build-depends: directory
1313 else
1414 if os(linux-android)
15− hs-source-dirs: src-android
15+ hs-source-dirs: src-android, src
1616 c-sources: cbits/ExecutableConfig.c
1717 other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
1818 build-depends: android-activity
1919 ld-options: -landroid
2020 else
2121 if impl(ghcjs)
22− hs-source-dirs: src-ghcjs
22+ hs-source-dirs: src-ghcjs, src
2323 build-depends: ghcjs-dom
2424 else
25− hs-source-dirs: src, src-directory
26− other-modules: Obelisk.ExecutableConfig.Internal.ConfigDirectory
25+ hs-source-dirs: src-other, src
26+ other-modules: Obelisk.Configs.Internal.Directory
2727 build-depends: directory
2828
2929 build-depends:
3030 base,
31+ base64-bytestring,
3132 bytestring,
3233 containers,
3334 filepath,
3435 text,
35− transformers
36+ transformers,
37+ transformers-base,
38+ ref-tf,
39+ primitive,
40+ monad-control,
41+ reflex,
42+ reflex-dom,
43+ jsaddle
3644
3745 ghc-options: -Wall
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hs+11−7
@@ -1,8 +1,10 @@
11{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
23module Obelisk.ExecutableConfig.Lookup where
34
45import Control.Exception (bracket)
56import Control.Monad (forM)
7+import Data.ByteString (ByteString)
68import qualified Data.ByteString as BS
79import Data.Map (Map)
810import qualified Data.Map as Map
@@ -13,7 +15,7 @@ import Foreign.Ptr (nullPtr)
1315
1416import Obelisk.ExecutableConfig.Internal.AssetManager
1517
16−getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe Text)
18+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe ByteString)
1719getFromMgr mgr name = do
1820 let open = do
1921 a <- BS.useAsCString name $ \fn ->
@@ -25,10 +27,10 @@ getFromMgr mgr name = do
2527 bracket open close $ mapM $ \asset -> do
2628 b <- asset_getBuffer asset
2729 l <- asset_getLength asset
28− fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
30+ BS.packCStringLen (b, fromIntegral l)
2931
30−getConfigs :: IO (Map Text Text)
31−getConfigs = bracket getAssets freeAssetManager $ \mgrObj -> do
32+getConfigs :: IO (Map Text ByteString)
33+getConfigs = fmap (Map.mapKeys T.decodeUtf8) $ bracket getAssets freeAssetManager $ \mgrObj -> do
3234 mgr <- assetManagerFromJava mgrObj
3335 let openDir = do
3436 d <- withCString "config.files" $ \fn ->
@@ -43,10 +45,12 @@ getConfigs = bracket getAssets freeAssetManager $ \mgrObj -> do
4345 l <- asset_getLength asset
4446 lines0 <$> BS.packCStringLen (b, fromIntegral l)
4547 Nothing -> error "could not open configuration manifest 'config.files'"
46− fmap Map.fromList $ forM configPaths $ \fp ->
47− getFromMgr mgr fp >>= \case
48− Just v -> return (T.decodeUtf8 fp, v)
48+ result <- fmap Map.fromList $ forM configPaths $ \fp ->
49+ getFromMgr mgr ("config/" <> fp) >>= \case
50+ Just v -> return (fp, v)
4951 Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
52+ putStrLn $ "getConfigs: found " <> show result
53+ pure result
5054
5155lines0 :: BS.ByteString -> [BS.ByteString]
5256lines0 ps
lib/executable-config/lookup/src-directory/Obelisk/ExecutableConfig/Internal/ConfigDirectory.hsdeleted−24
@@ -1,24 +0,0 @@
1−module Obelisk.ExecutableConfig.Internal.ConfigDirectory where
2−
3−import Data.Map (Map)
4−import qualified Data.Map as Map
5−import Data.Text (Text)
6−import Data.Text as T
7−import Data.Text.IO as T
8−import System.Directory
9−import System.FilePath.Posix ((</>))
10−
11−getConfigsFromDirectory :: FilePath -> FilePath -> IO (Map Text Text)
12−getConfigsFromDirectory base fp = do
13− dir <- doesDirectoryExist (base </> fp)
14− if dir
15− then do
16− ps <- listDirectory (base </> fp)
17− fmap mconcat $ mapM (\p -> getConfigsFromDirectory base $ fp </> p) ps
18− else do
19− file <- doesFileExist (base </> fp)
20− if file
21− then do
22− f <- T.readFile (base </> fp)
23− return $ Map.singleton (T.pack fp) f
24− else return mempty
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+25−10
@@ -2,35 +2,50 @@
22{-# LANGUAGE OverloadedStrings #-}
33module Obelisk.ExecutableConfig.Lookup where
44
5+import Data.ByteString (ByteString)
6+import qualified Data.ByteString.Base64 as B64
57import Data.Map (Map)
68import qualified Data.Map as Map
79import Data.Maybe (catMaybes)
810import Data.Text (Text)
11+import qualified Data.Text.Encoding as T
912import Data.Traversable (for)
1013import GHCJS.DOM
1114import GHCJS.DOM.Document (getHead)
1215import GHCJS.DOM.DOMStringMap (get)
1316import GHCJS.DOM.Element (getInnerHTML)
1417import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
15−import GHCJS.DOM.NodeList (item, getLength)
18+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
1619import GHCJS.DOM.ParentNode (querySelectorAll)
17−import GHCJS.DOM.Types (Node(Node), castTo)
20+import GHCJS.DOM.Types (MonadJSM, Node(Node), castTo)
1821
19−getConfigs :: IO (Map Text Text)
22+getConfigs :: IO (Map Text ByteString)
2023getConfigs = do
2124 Just doc <- currentDocument
2225 Just hd <- getHead doc
23− nodes <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
26+ nodes <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
2427 fmap Map.fromList $ for nodes $ \node -> do
2528 e <- castTo HTMLElement node >>= \case
2629 Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
2730 Just htmlE -> return htmlE
2831 dataset <- getDataset e
2932 (,)
30− <$> get dataset ("obelisk-executable-config-inject-key" :: Text)
31− <*> getInnerHTML e
33+ -- the key is camelCased: https://html.spec.whatwg.org/multipage/dom.html#dom-dataset
34+ <$> get dataset ("obeliskExecutableConfigInjectKey" :: Text)
35+ <*> (fmap decodeOrFail (getInnerHTML e))
3236 where
33− collToList es = do
34− len <- getLength es
35− list <- traverse (item es) [0..len-1]
36− pure $ catMaybes list
37+ decodeOrFail x = case B64.decode (T.encodeUtf8 x) of
38+ Left e -> error ("Obelisk.ExecutableConfig.Lookup.getConfigs: error when decoding base64: " ++ e)
39+ Right x' -> x'
40+
41+-- | Collect all nodes in the node list.
42+--
43+-- TODO: this and the version in obelisk-frontend should be
44+-- upstreamed to jsaddle.
45+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
46+nodeListNodes es = do
47+ len <- getLength es
48+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
49+ -- accidentally traversing over [0..maxBound::Word]
50+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
51+ pure $ catMaybes nodes
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hs+5−3
@@ -1,15 +1,17 @@
11{-# LANGUAGE LambdaCase #-}
22module Obelisk.ExecutableConfig.Lookup where
33
4+import Data.ByteString (ByteString)
45import Data.Map (Map)
56import Data.Text (Text)
67import qualified Data.Text as T
78import qualified Data.Text.Encoding as T
89import Language.Javascript.JSaddle.WKWebView
10+import System.FilePath.Posix
911
10−import Obelisk.ExecutableConfig.Internal.ConfigDirectory
12+import Obelisk.Configs.Internal.Directory
1113
12−getConfigs :: IO (Map Text Text)
14+getConfigs :: IO (Map Text ByteString)
1315getConfigs = mainBundleResourcePath >>= \case
1416 Nothing -> error "Could not get bundle resource path."
15− Just p -> getConfigsFromDirectory (T.unpack (T.decodeUtf8 p)) "config"
17+ Just p -> getConfigsFromDirectory $ T.unpack (T.decodeUtf8 p) </> "config"
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hsnew file+18
@@ -0,0 +1,18 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.Map (Map)
6+import qualified Data.Map as Map
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import Data.ByteString (ByteString)
10+import qualified Data.ByteString as BS
11+import System.Directory
12+import System.FilePath
13+import Control.Monad
14+
15+import Obelisk.Configs.Internal.Directory
16+
17+getConfigs :: IO (Map Text ByteString)
18+getConfigs = getConfigsFromDirectory "config"
lib/executable-config/lookup/src/Obelisk/Configs.hsnew file+124
@@ -0,0 +1,124 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.Configs
14+ ( HasConfigs(..)
15+ , ConfigsT
16+ , runConfigsT
17+ , mapConfigsT
18+ ) where
19+
20+import Control.Applicative
21+import Control.Monad
22+import Control.Monad.Base
23+import Control.Monad.Fix
24+import Control.Monad.Primitive
25+import Control.Monad.Ref
26+import Control.Monad.IO.Class
27+import Control.Monad.Trans.Class
28+import Control.Monad.Trans.Control
29+import Control.Monad.Trans.Reader
30+import Data.ByteString (ByteString)
31+import Data.Map (Map)
32+import qualified Data.Map as Map
33+import Data.Text (Text)
34+import Reflex
35+import Reflex.Host.Class
36+import Reflex.Dom
37+#ifndef ghcjs_HOST_OS
38+import Language.Javascript.JSaddle (MonadJSM)
39+#endif
40+
41+class Monad m => HasConfigs m where
42+ getConfigs :: m (Map Text ByteString)
43+ default getConfigs :: (HasConfigs m', m ~ t m', MonadTrans t) => m (Map Text ByteString)
44+ getConfigs = lift getConfigs
45+ getConfig :: Text -> m (Maybe ByteString)
46+ getConfig k = do
47+ configs <- getConfigs
48+ return $ Map.lookup k configs
49+
50+instance Monad m => HasConfigs (ConfigsT m) where
51+ getConfigs = ConfigsT ask
52+
53+instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
54+instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
55+instance HasConfigs m => HasConfigs (EventWriterT t w m)
56+instance HasConfigs m => HasConfigs (PostBuildT t m)
57+instance HasConfigs m => HasConfigs (QueryT t q m)
58+instance HasConfigs m => HasConfigs (ReaderT r m)
59+instance HasConfigs m => HasConfigs (RequesterT t request response m)
60+instance HasConfigs m => HasConfigs (StaticDomBuilderT t m)
61+instance HasConfigs m => HasConfigs (TriggerEventT t m)
62+
63+newtype ConfigsT m a = ConfigsT { unConfigsT :: ReaderT (Map Text ByteString) m a }
64+ deriving
65+ ( Functor
66+ , Applicative
67+ , Monad
68+ , MonadPlus
69+ , Alternative
70+ , MonadFix
71+ , MonadIO
72+ , MonadBase m'
73+ , MonadBaseControl m'
74+ , MonadRef
75+ , MonadTrans
76+ , DomBuilder t
77+ , MonadHold t
78+ , MonadReflexCreateTrigger t
79+ , MonadSample t
80+ , NotReady t
81+ , PostBuild t
82+ , TriggerEvent t
83+ , HasDocument
84+ , DomRenderHook t
85+ , HasJSContext
86+ , HasJS js
87+#ifndef ghcjs_HOST_OS
88+ , MonadJSM
89+#endif
90+ )
91+
92+instance PerformEvent t m => PerformEvent t (ConfigsT m) where
93+ type Performable (ConfigsT m) = ConfigsT (Performable m)
94+ performEvent e = ConfigsT $ ReaderT $ \configs ->
95+ performEvent $ runConfigsT configs <$> e
96+ performEvent_ e = ConfigsT $ ReaderT $ \configs ->
97+ performEvent_ $ runConfigsT configs <$> e
98+
99+instance Adjustable t m => Adjustable t (ConfigsT m) where
100+ runWithReplace a e = ConfigsT $ runWithReplace (unConfigsT a) (unConfigsT <$> e)
101+ traverseDMapWithKeyWithAdjust f m e = ConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
102+ traverseIntMapWithKeyWithAdjust f m e = ConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
103+ traverseDMapWithKeyWithAdjustWithMove f m e = ConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unConfigsT $ f k v) m e
104+
105+instance Prerender js t m => Prerender js t (ConfigsT m) where
106+ type Client (ConfigsT m) = ConfigsT (Client m)
107+ prerender server client = ConfigsT $ ReaderT $ \configs ->
108+ prerender (runConfigsT configs server) (runConfigsT configs client)
109+
110+instance PrimMonad m => PrimMonad (ConfigsT m) where
111+ type PrimState (ConfigsT m) = PrimState m
112+ primitive = lift . primitive
113+
114+runConfigsT
115+ :: Map Text ByteString
116+ -> ConfigsT m a
117+ -> m a
118+runConfigsT cs child = runReaderT (unConfigsT child) cs
119+
120+mapConfigsT
121+ :: (forall x. m x -> n x)
122+ -> ConfigsT m a
123+ -> ConfigsT n a
124+mapConfigsT f (ConfigsT x) = ConfigsT $ mapReaderT f x
lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hsnew file+24
@@ -0,0 +1,24 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.Configs.Internal.Directory where
4+
5+import Control.Monad
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString as BS
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Text (Text)
11+import qualified Data.Text as T
12+import System.Directory
13+import System.FilePath.Posix
14+
15+getConfigsFromDirectory :: FilePath -> IO (Map Text ByteString)
16+getConfigsFromDirectory base = doesDirectoryExist base >>= \case
17+ True -> do
18+ ps <- listDirectory base
19+ fmap mconcat $ forM ps $ \p -> do
20+ subdirConfigs <- getConfigsFromDirectory $ base </> p
21+ pure $ Map.mapKeys (T.pack . (p </>) . T.unpack) subdirConfigs
22+ False -> doesFileExist base >>= \case
23+ True -> Map.singleton "" <$> BS.readFile base
24+ False -> pure mempty
lib/executable-config/lookup/src/Obelisk/ExecutableConfig/Lookup.hsdeleted−9
@@ -1,9 +0,0 @@
1−module Obelisk.ExecutableConfig.Lookup where
2−
3−import Data.Map (Map)
4−import Data.Text (Text)
5−
6−import Obelisk.ExecutableConfig.Internal.ConfigDirectory
7−
8−getConfigs :: IO (Map Text Text)
9−getConfigs = getConfigsFromDirectory "." "config"
lib/frontend/obelisk-frontend.cabal−1
@@ -14,7 +14,6 @@ library
1414 jsaddle,
1515 lens,
1616 mtl,
17− obelisk-executable-config-frontend,
1817 obelisk-executable-config-inject,
1918 obelisk-executable-config-lookup,
2019 obelisk-route,
lib/frontend/src/Obelisk/Frontend.hs+120−38
@@ -1,5 +1,5 @@
1−{-# LANGUAGE ConstraintKinds #-}
21{-# LANGUAGE CPP #-}
2+{-# LANGUAGE ConstraintKinds #-}
33{-# LANGUAGE FlexibleContexts #-}
44{-# LANGUAGE FlexibleInstances #-}
55{-# LANGUAGE LambdaCase #-}
@@ -15,9 +15,10 @@ module Obelisk.Frontend
1515 ( ObeliskWidget
1616 , Frontend (..)
1717 , runFrontend
18− , runFrontendWithConfigs
18+ , runFrontendWithConfigsAndCurrentRoute
1919 , renderFrontendHtml
2020 , removeHTMLConfigs
21+ , FrontendMode (..)
2122 , module Obelisk.Frontend.Cookie
2223 ) where
2324
@@ -38,21 +39,27 @@ import Data.Map (Map)
3839import Data.Maybe (catMaybes)
3940import Data.Monoid ((<>))
4041import Data.Text (Text)
42+import qualified GHCJS.DOM as DOM
43+import qualified GHCJS.DOM.Types as DOM
44+import qualified GHCJS.DOM.History as DOM
45+import qualified GHCJS.DOM.Window as DOM
46+import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
4147import GHCJS.DOM (currentDocument)
4248import GHCJS.DOM.Document (getHead)
43−import GHCJS.DOM.Node (removeChild_)
44−import GHCJS.DOM.NodeList (item, getLength)
49+import GHCJS.DOM.Node (Node, removeChild_)
50+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
4551import GHCJS.DOM.ParentNode (querySelectorAll)
46−import Language.Javascript.JSaddle (JSM)
4752import Obelisk.Frontend.Cookie
4853import Obelisk.Route.Frontend
4954import Reflex.Dom.Core
5055import Reflex.Host.Class
51−import Obelisk.ExecutableConfig.Frontend
56+import Obelisk.Configs
5257import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
53−import Obelisk.ExecutableConfig.Lookup (getConfigs)
58+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
5459import Web.Cookie
5560
61+import Debug.Trace
62+
5663makePrisms ''Sum
5764
5865type ObeliskWidget js t route m =
@@ -74,7 +81,7 @@ type ObeliskWidget js t route m =
7481 , Prerender js t m
7582 , PrebuildAgnostic t route m
7683 , PrebuildAgnostic t route (Client m)
77− , HasFrontendConfigs m
84+ , HasConfigs m
7885 , HasCookies m
7986 )
8087
@@ -82,8 +89,8 @@ type PrebuildAgnostic t route m =
8289 ( SetRoute t route m
8390 , RouteToUrl route m
8491 , MonadFix m
85− , HasFrontendConfigs m
86− , HasFrontendConfigs (Performable m)
92+ , HasConfigs m
93+ , HasConfigs (Performable m)
8794 )
8895
8996data Frontend route = Frontend
@@ -98,57 +105,132 @@ removeHTMLConfigs :: JSM ()
98105removeHTMLConfigs = void $ runMaybeT $ do
99106 doc <- MaybeT currentDocument
100107 hd <- MaybeT $ getHead doc
101− es <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
108+ es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
102109 for_ es $ removeChild_ hd
103− where
104− collToList es = do
105− len <- getLength es
106− lst <- traverse (item es) $ take (fromIntegral len) $ [0..] -- fun with unsigned types ...
107− pure $ catMaybes lst
108110
111+-- | Collect all nodes in the node list.
112+--
113+-- TODO: this and the version in exe-config/ghcjs/lookup should be
114+-- upstreamed to jsaddle.
115+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
116+nodeListNodes es = do
117+ len <- getLength es
118+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
119+ -- accidentally traversing over [0..maxBound::Word]
120+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
121+ pure $ catMaybes nodes
122+
123+setInitialRoute :: Bool -> JSM ()
124+setInitialRoute useHash = do
125+ traceM "setInitialRoute"
126+ window <- DOM.currentWindowUnchecked
127+ initialLocation <- DOM.getLocation window
128+ initialUri <- getLocationUri initialLocation
129+ history <- DOM.getHistory window
130+ DOM.replaceState history jsNull ("" :: Text) $ Just $
131+ show $ setAdaptedUriPath useHash "/" initialUri
132+
133+data FrontendMode = FrontendMode
134+ { _frontendMode_hydrate :: Bool
135+ -- ^ There is already a rendering of the DOM in place; hydrate it rather
136+ -- than building new DOM
137+ , _frontendMode_adjustRoute :: Bool
138+ -- ^ The page can't use regular routes, so encode routes into the hash
139+ -- instead
140+ }
141+
142+-- | Run the frontend, setting the initial route to "/" on platforms where no
143+-- route exists ambiently in the context (e.g. anything but web).
144+-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
109145runFrontend
110146 :: forall backendRoute route
111147 . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
112148 -> Frontend (R route)
113149 -> JSM ()
114150runFrontend validFullEncoder frontend = do
115− configs <- liftIO getConfigs
151+ let mode = FrontendMode
152+ { _frontendMode_hydrate =
153+#ifdef ghcjs_HOST_OS
154+ True
155+#else
156+ False
157+#endif
158+ , _frontendMode_adjustRoute =
116159#ifdef ghcjs_HOST_OS
117− removeHTMLConfigs
160+ False
161+#else
162+ True
118163#endif
119− runFrontendWithConfigs configs validFullEncoder frontend
164+ }
165+ configs <- liftIO Lookup.getConfigs
166+ when (_frontendMode_hydrate mode) removeHTMLConfigs
167+ -- There's no fundamental reason that adjustRoute needs to control setting the
168+ -- initial route and *also* the useHash parameter; that's why these are
169+ -- separate here. However, currently, they are always the same.
170+ when (_frontendMode_adjustRoute mode) $ do
171+ setInitialRoute $ _frontendMode_adjustRoute mode
172+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
120173
121−runFrontendWithConfigs
174+runFrontendWithConfigsAndCurrentRoute
122175 :: forall backendRoute route
123− . Map Text Text
176+ . FrontendMode
177+ -> Map Text ByteString
124178 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
125179 -> Frontend (R route)
126180 -> JSM ()
127−runFrontendWithConfigs configs validFullEncoder frontend = do
181+runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
128182 let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
129183 errorLeft = \case
130184 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
131185 Right x -> Identity x
132− runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
133− rec switchover <- runRouteViewT ve switchover $ do
134− (switchover'', fire) <- newTriggerEvent
135− mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runFrontendConfigsT configs))) $ do
136− -- The order here is important - baseTag has to be before headWidget!
137− baseTag
138− _frontend_head frontend
139− mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runFrontendConfigsT configs))) $ do
140− _frontend_body frontend
141− switchover' <- lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
142− performEvent_ $ liftIO (fire ()) <$ switchover'
143− pure switchover''
144− pure ()
186+ w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
187+ , Ref (Performable m) ~ Ref IO
188+ , Ref m ~ Ref IO
189+ , DomBuilder DomTimeline (HydrationDomBuilderT s DomTimeline m)
190+ , MonadHold DomTimeline m
191+ , MonadRef m
192+ , MonadRef (Performable m)
193+ , MonadReflexCreateTrigger DomTimeline m
194+ , PerformEvent DomTimeline m
195+ , PostBuild DomTimeline m
196+ , PrimMonad m
197+ , MonadSample DomTimeline (Performable m)
198+ , DOM.MonadJSM m
199+ , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
200+ , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
201+ , MonadFix (Performable m)
202+ , MonadFix m
203+ , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
204+ , MonadIO (Performable m)
205+ )
206+ => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
207+ -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
208+ -> FloatingWidget () ()
209+ w appendHead appendBody = do
210+ rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
211+ (switchover'', fire) <- newTriggerEvent
212+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runConfigsT configs))) $ do
213+ -- The order here is important - baseTag has to be before headWidget!
214+ baseTag
215+ _frontend_head frontend
216+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runConfigsT configs))) $ do
217+ _frontend_body frontend
218+ switchover' <- case _frontendMode_hydrate mode of
219+ True -> lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
220+ False -> getPostBuild
221+ performEvent_ $ liftIO (fire ()) <$ switchover'
222+ pure switchover''
223+ pure ()
224+ if _frontendMode_hydrate mode
225+ then runHydrationWidgetWithHeadAndBody (pure ()) w
226+ else runImmediateWidgetWithHeadAndBody w
145227
146228renderFrontendHtml
147229 :: ( t ~ DomTimeline
148230 , MonadIO m
149− , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (FrontendConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
231+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
150232 )
151− => Map Text Text
233+ => Map Text ByteString
152234 -> Cookies
153235 -> (r -> Text)
154236 -> r
@@ -158,7 +240,7 @@ renderFrontendHtml
158240 -> m ByteString
159241renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
160242 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
161− html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runFrontendConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
243+ html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
162244 el "html" $ do
163245 el "head" $ do
164246 baseTag
lib/frontend/src/Obelisk/Frontend/Cookie.hs+3−3
@@ -26,7 +26,7 @@ import GHCJS.DOM.Document (getCookie, Document)
2626import GHCJS.DOM.Types (MonadJSM)
2727import Web.Cookie
2828
29−import Obelisk.ExecutableConfig.Frontend
29+import Obelisk.Configs
3030import Obelisk.Route.Frontend
3131
3232class Monad m => HasCookies m where
@@ -46,8 +46,8 @@ instance HasCookies m => HasCookies (SetRouteT t r m)
4646instance HasCookies m => HasCookies (StaticDomBuilderT t m)
4747instance HasCookies m => HasCookies (TriggerEventT t m)
4848instance HasCookies m => HasCookies (RoutedT t r m)
49−instance HasCookies m => HasCookies (FrontendConfigsT m)
50−instance HasFrontendConfigs m => HasFrontendConfigs (CookiesT m)
49+instance HasCookies m => HasCookies (ConfigsT m)
50+instance HasConfigs m => HasConfigs (CookiesT m)
5151
5252newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
5353 deriving
lib/route/obelisk-route.cabal+7−3
@@ -6,12 +6,14 @@ build-type: Simple
66library
77 hs-source-dirs: src
88 build-depends: base,
9+ aeson,
10+ bytestring,
911 categories,
1012 constraints,
13+ constraints-extras,
1114 containers,
1215 dependent-map,
1316 dependent-sum,
14− dependent-sum-universe-orphans,
1517 dependent-sum-template,
1618 either,
1719 ghcjs-dom,
@@ -21,16 +23,18 @@ library
2123 monad-control,
2224 mtl,
2325 network-uri,
24− obelisk-executable-config-frontend,
26+ obelisk-executable-config-lookup,
2527 primitive,
2628 ref-tf,
2729 reflex,
2830 reflex-dom-core,
2931 template-haskell,
32+ tabulation,
3033 text,
34+ th-extras,
3135 transformers,
3236 universe,
33− universe-template
37+ universe-dependent-sum
3438 exposed-modules: Obelisk.Route
3539 Obelisk.Route.TH
3640 Obelisk.Route.Frontend
lib/route/src/Obelisk/Route.hs+107−32
@@ -13,6 +13,7 @@
1313{-# LANGUAGE RankNTypes #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
1515{-# LANGUAGE TemplateHaskell #-}
16+{-# LANGUAGE TypeApplications #-}
1617{-# LANGUAGE TypeFamilies #-}
1718{-# LANGUAGE TypeOperators #-}
1819{-# LANGUAGE ViewPatterns #-}
@@ -85,6 +86,11 @@ module Obelisk.Route
8586 , readShowEncoder
8687 , integralEncoder
8788 , pathSegmentEncoder
89+ , queryOnlyEncoder
90+ , Decoder(..)
91+ , dmapEncoder
92+ , fieldMapEncoder
93+ , jsonEncoder
8894 ) where
8995
9096import Prelude hiding ((.), id)
@@ -97,6 +103,7 @@ import Control.Category.Associative
97103import Control.Category.Monoidal
98104import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
99105import Control.Monad.Except
106+import qualified Data.ByteString.Lazy as BSL
100107import Data.Dependent.Sum (DSum (..))
101108import Data.Dependent.Map (DMap)
102109import qualified Data.Dependent.Map as DMap
@@ -113,17 +120,19 @@ import Data.Maybe
113120import Data.Monoid ((<>))
114121import Data.Set (Set)
115122import qualified Data.Set as Set
116−import Data.Some (Some)
117−import qualified Data.Some as Some
118−import Data.Some.Universe.Orphans ()
123+import Data.Some (Some(Some))
119124import Data.Text (Text)
120125import qualified Data.Text as T
121−import Data.Text.Encoding
126+import qualified Data.Text.Encoding as T
122127import Data.Universe
128+import Data.Universe.Some
123129import Network.HTTP.Types.URI
124130import qualified Numeric.Lens
125131import Obelisk.Route.TH
126132import Text.Read (readMaybe)
133+import Data.Tabulation
134+import qualified Data.Aeson as Aeson
135+import Data.Aeson (FromJSON, ToJSON)
127136
128137-- Design goals:
129138-- No start-up time on the frontend (not yet met)
@@ -155,7 +164,7 @@ pattern (:/) :: f a -> a -> R f
155164pattern a :/ b = a :=> Identity b
156165
157166mapSome :: (forall a. f a -> g a) -> Some f -> Some g
158−mapSome f (Some.This a) = Some.This $ f a
167+mapSome f (Some a) = Some $ f a
159168
160169hoistR :: (forall x. f x -> g x) -> R f -> R g
161170hoistR f (x :=> Identity y) = f x :/ y
@@ -193,7 +202,7 @@ decode e x = runIdentity (tryDecode e x)
193202tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
194203tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
195204
196−-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
205+-- | Similar to 'decode', once an encoder has been checked so that its check monad is Identity, it
197206-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
198207-- one should usually be applying decode and encode to the same 'Encoder'
199208encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
@@ -343,14 +352,14 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
343352justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
344353justEncoder = prismEncoder _Just
345354
346−-- |
355+-- | Encode () to 'Nothing'.
347356nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
348357nothingEncoder = prismEncoder _Nothing
349358
350359someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
351360someConstEncoder = unsafeMkEncoder $ EncoderImpl
352− { _encoderImpl_encode = \(Some.This (Const a)) -> a
353− , _encoderImpl_decode = pure . Some.This . Const
361+ { _encoderImpl_encode = \(Some (Const a)) -> a
362+ , _encoderImpl_decode = pure . Some . Const
354363 }
355364
356365-- | WARNING: This is only safe if the Show and Read instances for 'a' are
@@ -378,7 +387,7 @@ checkEnum1EncoderFunc
378387 -> check (EncoderFunc check' parse p r)
379388checkEnum1EncoderFunc f = do
380389 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
381− traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
390+ traverse (\(Some p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
382391 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
383392 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
384393
@@ -433,11 +442,11 @@ chainEncoder cons this rest = Encoder $ do
433442 pure $ EncoderImpl
434443 { _encoderImpl_decode = \v -> do
435444 (here, following) <- _encoderImpl_decode consValid v
436− Some.This r <- _encoderImpl_decode thisValid here
445+ Some r <- _encoderImpl_decode thisValid here
437446 (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
438447 , _encoderImpl_encode = \(r :/ s) ->
439448 _encoderImpl_encode consValid
440− ( _encoderImpl_encode thisValid $ Some.This r
449+ ( _encoderImpl_encode thisValid $ Some r
441450 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
442451 }
443452
@@ -504,7 +513,7 @@ enum1Encoder
504513 , Show r
505514 )
506515 => (forall a. p a -> r) -> Encoder check parse (Some p) r
507−enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
516+enum1Encoder f = enumEncoder $ \(Some p) -> f p
508517
509518-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
510519-- have a large number of values - it will use a lot of memory.
@@ -547,13 +556,10 @@ pathOnlyEncoderIgnoringQuery = unsafeMkEncoder $ EncoderImpl
547556 }
548557
549558pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
550−pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
551− { _encoderImpl_decode = \(path, query) ->
552− if query == mempty
553− then pure path
554− else throwError "pathOnlyEncoderImpl: query was provided"
555− , _encoderImpl_encode = \path -> (path, mempty)
556− }
559+pathOnlyEncoder = second (unitEncoder mempty) . coidr
560+
561+queryOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Map Text (Maybe Text)) PageName
562+queryOnlyEncoder = first (unitEncoder []) . coidl
557563
558564singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
559565singletonListEncoder = unsafeMkEncoder $ EncoderImpl
@@ -596,10 +602,10 @@ queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
596602 in (urlDecodeText True k, urlDecodeText True <$> mv)
597603
598604urlEncodeText :: Bool -> Text -> Text
599−urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
605+urlEncodeText q = T.decodeUtf8 . urlEncode q . T.encodeUtf8
600606
601607urlDecodeText :: Bool -> Text -> Text
602−urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
608+urlDecodeText q = T.decodeUtf8 . urlDecode q . T.encodeUtf8
603609
604610listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
605611listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
@@ -711,9 +717,11 @@ data ObeliskRoute :: (* -> *) -> * -> * where
711717 ObeliskRoute_App :: f a -> ObeliskRoute f a
712718 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
713719
714−instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
715− universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
716− ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
720+instance UniverseSome f => UniverseSome (ObeliskRoute f) where
721+ universeSome = concat
722+ [ (\(Some x) -> Some (ObeliskRoute_App x)) <$> universe
723+ , (\(Some x) -> Some (ObeliskRoute_Resource x)) <$> universe
724+ ]
717725
718726instance GEq f => GEq (ObeliskRoute f) where
719727 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
@@ -801,12 +809,12 @@ indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
801809
802810someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
803811someSumEncoder = Encoder $ pure $ EncoderImpl
804− { _encoderImpl_encode = \(Some.This t) -> case t of
805− InL l -> Left $ Some.This l
806− InR r -> Right $ Some.This r
812+ { _encoderImpl_encode = \(Some t) -> case t of
813+ InL l -> Left $ Some l
814+ InR r -> Right $ Some r
807815 , _encoderImpl_decode = pure . \case
808− Left (Some.This l) -> Some.This (InL l)
809− Right (Some.This r) -> Some.This (InR r)
816+ Left (Some l) -> Some (InL l)
817+ Right (Some r) -> Some (InR r)
810818 }
811819
812820data Void1 :: * -> * where {}
@@ -817,7 +825,7 @@ instance Universe (Some Void1) where
817825void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
818826void1Encoder = Encoder $ pure $ EncoderImpl
819827 { _encoderImpl_encode = \case
820− Some.This f -> case f of {}
828+ Some f -> case f of {}
821829 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
822830 }
823831
@@ -866,7 +874,6 @@ renderObeliskRoute e r =
866874readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
867875readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
868876
869−
870877integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
871878integralEncoder = prismEncoder (Numeric.Lens.integral)
872879
@@ -879,4 +886,72 @@ pathSegmentEncoder = unsafeMkEncoder EncoderImpl
879886 (x:xs) -> pure (x, (xs, y))
880887 }
881888
889+newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
890+
891+dmapEncoder :: forall check parse k k' v.
892+ ( Monad check
893+ , MonadError Text parse
894+ , Universe (Some k')
895+ , Ord k
896+ , GCompare k'
897+ , GShow k'
898+ )
899+ => Encoder check parse (Some k') k
900+ -> (forall v'. k' v' -> Encoder check parse v' v)
901+ -> Encoder check parse (DMap k' Identity) (Map k v)
902+dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
903+ keyEncoder :: Encoder Identity parse (Some k') k <- checkEncoder keyEncoder'
904+ valueDecoders :: DMap k' (Decoder Identity parse v) <- fmap DMap.fromList . forM universe $ \(Some (k' :: k' t)) -> do
905+ ve :: Encoder Identity parse t v <- checkEncoder (valueEncoderFor k')
906+ return $ (k' :: k' t) :=> (Decoder ve :: Decoder Identity parse v t)
907+ let keyError k = "dmapEncoder: key `" <> k <> "' was missing from the Universe instance for its type."
908+ return $ EncoderImpl
909+ { _encoderImpl_encode = \dm -> Map.fromList $ do
910+ ((k' :: k' t) :=> Identity v') <- DMap.toList dm
911+ return ( encode keyEncoder (Some k')
912+ , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
913+ )
914+ , _encoderImpl_decode = \m -> fmap DMap.fromList . forM (Map.toList m) $ \(k,v) -> do
915+ Some (k' :: k' t) <- tryDecode keyEncoder k
916+ case DMap.lookup k' valueDecoders of
917+ Nothing -> throwError . T.pack . keyError $ gshow k'
918+ Just (Decoder e) -> do
919+ v' <- tryDecode e v
920+ return (k' :=> Identity v')
921+ }
922+
923+fieldMapEncoder :: forall check parse r.
924+ ( Applicative check
925+ , MonadError Text parse
926+ , HasFields r
927+ , Universe (Some (Field r))
928+ , GShow (Field r)
929+ , GCompare (Field r)
930+ )
931+ => Encoder check parse r (DMap (Field r) Identity)
932+fieldMapEncoder = unsafeEncoder $ do
933+ pure $ EncoderImpl
934+ { _encoderImpl_encode = \r -> DMap.fromList [ f :=> Identity (indexField r f) | Some f <- universe ]
935+ , _encoderImpl_decode = \dm -> tabulateFieldsA $ \f -> do
936+ case DMap.lookup f dm of
937+ Nothing -> throwError $ "fieldMapEncoder: Couldn't find key for `" <> T.pack (gshow f) <> "' in DMap."
938+ Just (Identity v) -> return v
939+ }
940+
941+-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
942+jsonEncoder :: forall check parse r.
943+ ( ToJSON r
944+ , FromJSON r
945+ , Applicative check
946+ , MonadError Text parse
947+ )
948+ => Encoder check parse r Text
949+jsonEncoder = unsafeEncoder $ do
950+ pure $ EncoderImpl
951+ { _encoderImpl_encode = \r -> T.decodeUtf8 . BSL.toStrict $ Aeson.encode r
952+ , _encoderImpl_decode = \t -> case Aeson.eitherDecodeStrict $ T.encodeUtf8 t of
953+ Left err -> throwError ("jsonEncoder: " <> T.pack err)
954+ Right x -> return x
955+ }
956+
882957--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+24−24
@@ -52,11 +52,12 @@ module Obelisk.Route.Frontend
5252 , mapRouteToUrlT
5353 , routeLink
5454 , dynRouteLink
55+ , adaptedUriPath
56+ , setAdaptedUriPath
5557 ) where
5658
5759import Prelude hiding ((.), id)
5860
59−import Obelisk.ExecutableConfig.Frontend
6061import Obelisk.Route
6162
6263import Control.Category (Category (..), (.))
@@ -90,13 +91,13 @@ import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can
9091import Reflex.Dom.Core
9192import qualified GHCJS.DOM.Types as DOM
9293import Network.URI
93−#if defined(ios_HOST_OS)
9494import Data.Maybe (fromMaybe)
9595import qualified Data.List as L
96−#endif
9796
9897import Unsafe.Coerce
9998
99+import Obelisk.Configs
100+
100101infixr 5 :~
101102pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
102103pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
@@ -178,7 +179,7 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
178179 askQueryResult = lift askQueryResult
179180 queryIncremental = lift . queryIncremental
180181
181−instance HasFrontendConfigs m => HasFrontendConfigs (RoutedT t r m)
182+instance HasConfigs m => HasConfigs (RoutedT t r m)
182183
183184instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
184185
@@ -342,7 +343,7 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
342343 type PrimState (SetRouteT t r m) = PrimState m
343344 primitive = lift . primitive
344345
345−instance HasFrontendConfigs m => HasFrontendConfigs (SetRouteT t r m)
346+instance HasConfigs m => HasConfigs (SetRouteT t r m)
346347
347348instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
348349 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
@@ -441,7 +442,7 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
441442 askQueryResult = lift askQueryResult
442443 queryIncremental = lift . queryIncremental
443444
444−instance HasFrontendConfigs m => HasFrontendConfigs (RouteToUrlT t m)
445+instance HasConfigs m => HasConfigs (RouteToUrlT t m)
445446
446447runRouteViewT
447448 :: forall t m r a.
@@ -453,17 +454,20 @@ runRouteViewT
453454 , MonadFix m
454455 )
455456 => (Encoder Identity Identity r PageName)
457+ --TODO: Get rid of the switchover and useHash arguments
458+ -- useHash can probably be baked into the encoder
456459 -> Event t () -- ^ Switchover event, nothing is done until this event fires. Used to prevent incorrect DOM expectations at hydration switchover time
460+ -> Bool
457461 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
458462 -> m a
459−runRouteViewT routeEncoder switchover a = do
463+runRouteViewT routeEncoder switchover useHash a = do
460464 rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
461465 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
462466 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
463467 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
464468 -- this ever to happen, we'll just handle it by failing completely with 'error'.
465469 route :: Dynamic t r
466− route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
470+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath useHash &&& uriQuery) . _historyItem_uri) historyState
467471 where
468472 errorLeft (Left e) = error (T.unpack e)
469473 errorLeft (Right x) = x
@@ -484,7 +488,7 @@ runRouteViewT routeEncoder switchover a = do
484488 -- we can change this function later to accommodate.
485489 -- See: https://github.com/whatwg/html/issues/2174
486490 , _historyStateUpdate_title = ""
487− , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
491+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath useHash newPath $ (_historyItem_uri currentHistoryState)
488492 { uriQuery = newQuery
489493 }
490494 }
@@ -537,22 +541,18 @@ dynRouteLink dr w = do
537541-- On ios due to sandboxing when loading the page from a file adapt the
538542-- path to be based on the hash.
539543
540−adaptedUriPath :: URI -> String
541−#if defined(ios_HOST_OS)
542−adaptedUriPath = hashToPath . uriFragment
543−
544−hashToPath :: String -> String
545−hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
546−#else
547−adaptedUriPath = uriPath
548−#endif
544+adaptedUriPath :: Bool -> URI -> String
545+adaptedUriPath = \case
546+ True -> hashToPath . uriFragment
547+ False -> uriPath
549548
550−setAdaptedUriPath :: String -> URI -> URI
551−#if defined(ios_HOST_OS)
552−setAdaptedUriPath s u = u { uriFragment = pathToHash s }
549+setAdaptedUriPath :: Bool -> String -> URI -> URI
550+setAdaptedUriPath useHash s u = case useHash of
551+ True -> u { uriFragment = pathToHash s }
552+ False -> u { uriPath = s }
553553
554554pathToHash :: String -> String
555555pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
556−#else
557−setAdaptedUriPath s u = u { uriPath = s }
558−#endif
556+
557+hashToPath :: String -> String
558+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
lib/route/src/Obelisk/Route/TH.hs+4−5
@@ -1,8 +1,9 @@
11module Obelisk.Route.TH (deriveRouteComponent) where
22
3+import Data.Constraint.Extras.TH
34import Data.GADT.Show.TH
45import Data.GADT.Compare.TH
5−import Data.Universe.TH
6+import Data.Universe.Some.TH
67import Language.Haskell.TH
78
89-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
@@ -11,8 +12,6 @@ deriveRouteComponent x = concat <$> traverse ($ x)
1112 [ deriveGShow
1213 , deriveGEq
1314 , deriveGCompare
14− , deriveShowTagIdentity
15− , deriveEqTagIdentity
16− , deriveOrdTagIdentity
17− , deriveSomeUniverse
15+ , deriveUniverseSome
16+ , deriveArgDict
1817 ]
lib/run/obelisk-run.cabal+1−1
@@ -26,7 +26,7 @@ library
2626 , network
2727 , obelisk-asset-serve-snap
2828 , obelisk-backend
29− , obelisk-executable-config-backend
29+ , obelisk-executable-config-lookup
3030 , obelisk-frontend
3131 , obelisk-route
3232 , process
lib/run/src/Obelisk/Run.hs+27−30
@@ -34,6 +34,7 @@ import Data.Streaming.Network (bindPortTCP)
3434import Data.String (fromString)
3535import Data.Text (Text)
3636import qualified Data.Text as T
37+import qualified Data.Text.Encoding as T
3738import Language.Javascript.JSaddle.Run (syncPoint)
3839import Language.Javascript.JSaddle.WebSockets
3940import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -48,7 +49,6 @@ import Network.WebSockets (ConnectionOptions)
4849import Network.WebSockets.Connection (defaultConnectionOptions)
4950import qualified Obelisk.Asset.Serve.Snap as Snap
5051import Obelisk.Backend
51−import Obelisk.ExecutableConfig.Backend
5252import Obelisk.Frontend
5353import Obelisk.Route.Frontend
5454import Reflex.Dom.Core
@@ -75,45 +75,39 @@ run port serveStaticAsset backend frontend = do
7575 case checkEncoder $ _backend_routeEncoder backend of
7676 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
7777 Right validFullEncoder -> do
78− configs <- getComponentConfigs
78+ publicConfigs <- getPublicConfigs
7979 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
80− runBackendConfigsT (_componentConfigs_backend configs) $
81− _backend_run backend $ \serveRoute -> do
82− runSnapWithCommandLineArgs $ do
83− getRouteWith validFullEncoder >>= \case
84− Identity r -> case r of
85− InL backendRoute :=> Identity a ->
86− runBackendConfigsT (_componentConfigs_backend configs) $
87− serveRoute $ backendRoute :/ a
88− InR obeliskRoute :=> Identity a ->
89− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend (_componentConfigs_frontend configs) $ obeliskRoute :/ a
80+ _backend_run backend $ \serveRoute -> do
81+ runSnapWithCommandLineArgs $ do
82+ getRouteWith validFullEncoder >>= \case
83+ Identity r -> case r of
84+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85+ InR obeliskRoute :=> Identity a ->
86+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
9087 let conf = defRunConfig { _runConfig_redirectPort = port }
91− runWidget conf (_componentConfigs_frontend configs) frontend validFullEncoder `finally` killThread backendTid
88+ runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
9289
9390-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
9491runServeAsset :: FilePath -> [Text] -> Snap ()
9592runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
9693
97−getConfigRoute :: Map Text Text -> IO (Maybe URI)
94+getConfigRoute :: Map Text ByteString -> Either Text URI
9895getConfigRoute configs = case Map.lookup "common/route" configs of
99− Just r -> case URI.mkURI $ T.strip r of
100− Just route -> pure $ Just route
101− Nothing -> do
102− putStrLn $ "Route is invalid: " <> show r
103− pure Nothing
104− Nothing -> pure Nothing
105−
106−defAppUri :: URI
107−defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
96+ Just r ->
97+ let stripped = T.strip (T.decodeUtf8 r)
98+ in case URI.mkURI stripped of
99+ Just route -> Right route
100+ Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
101+ Nothing -> Left $ "Couldn't find config file common/route; it should contain the site's canonical root URI" <> T.pack (show $ Map.keys configs)
108102
109103runWidget
110104 :: RunConfig
111− -> Map Text Text
105+ -> Map Text ByteString
112106 -> Frontend (R route)
113107 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
114108 -> IO ()
115109runWidget conf configs frontend validFullEncoder = do
116− uri <- fromMaybe defAppUri <$> getConfigRoute configs
110+ uri <- either (fail . T.unpack) pure $ getConfigRoute configs
117111 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
118112 redirectHost = _runConfig_redirectHost conf
119113 redirectPort = _runConfig_redirectPort conf
@@ -130,7 +124,7 @@ runWidget conf configs frontend validFullEncoder = do
130124
131125obeliskApp
132126 :: forall route backendRoute
133− . Map Text Text
127+ . Map Text ByteString
134128 -> ConnectionOptions
135129 -> Frontend (R route)
136130 -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
@@ -138,9 +132,12 @@ obeliskApp
138132 -> Application
139133 -> IO Application
140134obeliskApp configs opts frontend validFullEncoder uri backend = do
141− let entryPoint = do
142− removeHTMLConfigs
143− runFrontendWithConfigs configs validFullEncoder frontend
135+ let mode = FrontendMode
136+ { _frontendMode_hydrate = True
137+ , _frontendMode_adjustRoute = False
138+ }
139+ entryPoint = do
140+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
144141 syncPoint
145142 jsaddlePath <- URI.mkPathPiece "jsaddle"
146143 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
@@ -160,7 +157,7 @@ obeliskApp configs opts frontend validFullEncoder uri backend = do
160157 _ -> backend req sendResponse
161158
162159renderJsaddleFrontend
163− :: Map Text Text
160+ :: Map Text ByteString
164161 -> Cookies
165162 -> (route -> Text)
166163 -> route
lib/selftest/src/Obelisk/SelfTest.hs+12−10
@@ -75,6 +75,7 @@ main = do
7575 unless isVerbose $
7676 putStrLn "Tests may take longer to run if there are unbuilt derivations: use -v for verbose output"
7777 let verbosity = bool silently verbosely isVerbose
78+ nixBuild args = run "nix-build" ("--no-out-link" : args)
7879 obeliskImpl <- fromString <$> getEnv "OBELISK_IMPL"
7980 httpManager <- HTTP.newManager HTTP.defaultManagerSettings
8081 [p0, p1, p2, p3] <- liftIO $ getFreePorts 4
@@ -129,7 +130,8 @@ main = do
129130
130131 it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
131132 configs <- getConfigs
132− withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute configs `shouldNotReturn` Nothing
133+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $
134+ return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
133135
134136 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
135137 -- when not run by 'nix-build --attr selftest'
@@ -140,22 +142,22 @@ main = do
140142 testObRunInDir p2 p3 (Just "frontend") httpManager
141143
142144 describe "obelisk project" $ parallel $ do
143− it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
144− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
145− it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
145+ it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , obeliskImpl]
146+ it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", obeliskImpl]
147+ it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", obeliskImpl]
146148 -- See https://github.com/obsidiansystems/obelisk/issues/101
147− -- it "can build everything" $ shelly_ $ run "nix-build" [obeliskImpl]
149+ -- it "can build everything" $ shelly_ $ nixBuild [obeliskImpl]
148150
149151 describe "blank initialized project" $ parallel $ do
150152
151153 it "can build ghc.backend" $ inTmpObInit $ \_ -> do
152− run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
154+ nixBuild ["-A", "ghc.backend"]
153155 it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
154− run "nix-build" ["--no-out-link", "-A", "ghcjs.frontend"]
156+ nixBuild ["-A", "ghcjs.frontend"]
155157
156158 if os == "darwin"
157− then it "can build ios" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "ios.frontend" ]
158− else it "can build android" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "android.frontend"]
159+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
160+ else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
159161
160162 forM_ ["ghc", "ghcjs"] $ \compiler -> do
161163 let
@@ -165,7 +167,7 @@ main = do
165167 it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
166168
167169 it "can build reflex project" $ inTmpObInit $ \_ -> do
168− run "nix-build" []
170+ nixBuild []
169171
170172 it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
171173 u <- update
lib/tabulation/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for tabulation
2+
3+## 0.1.0.0 -- 2019-05-24
4+
5+* First version. Basically carve out this library as a place for the HasFields class.
lib/tabulation/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2019, Cale Gibbard
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Cale Gibbard nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lib/tabulation/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
lib/tabulation/src/Data/Tabulation.hsnew file+21
@@ -0,0 +1,21 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE RankNTypes #-}
3+
4+module Data.Tabulation where
5+
6+import Control.Lens
7+
8+-- | This is a class for record types whose fields can be enumerated by an associated GADT. It's closely related to the concept of a representable functor, except without the functor part, and the fields are not all the same type.
9+class HasFields a where
10+ type Field a :: * -> *
11+
12+ fieldLens :: Field a x -> Lens' a x
13+ tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
14+
15+ tabulateFields :: (forall x. Field a x -> x) -> a
16+ tabulateFields f = runIdentity (tabulateFieldsA (Identity . f))
17+ traverseWithField :: Applicative m => (forall x. Field a x -> x -> m x) -> a -> m a
18+ traverseWithField t r = tabulateFieldsA (\f -> t f (indexField r f))
19+ indexField :: a -> Field a x -> x
20+ indexField a f = a ^. fieldLens f
21+ {-# MINIMAL fieldLens, tabulateFieldsA #-}
lib/tabulation/tabulation.cabalnew file+26
@@ -0,0 +1,26 @@
1+cabal-version: >=1.10
2+-- Initial package description 'tabulation.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: tabulation
7+version: 0.1.0.0
8+synopsis: A library for types that can be completely reconstructed by fields as specified by a GADT.
9+-- description:
10+-- bug-reports:
11+license: BSD3
12+license-file: LICENSE
13+author: Cale Gibbard
14+maintainer: cale.gibbard@obsidian.systems
15+-- copyright:
16+category: Data
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+library
21+ exposed-modules: Data.Tabulation
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base, lens
25+ hs-source-dirs: src
26+ default-language: Haskell2010
release.nix+4−1
@@ -61,7 +61,10 @@ let
6161 ];
6262 command = local-self.command;
6363 serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
64− androidSkeleton = (import ./skeleton { obelisk = local-self; }).android.frontend;
64+ androidObelisk = import ./. (self-args // {
65+ config.android_sdk.accept_license = true;
66+ });
67+ androidSkeleton = (import ./skeleton { obelisk = androidObelisk; }).android.frontend;
6568 iosObelisk = import ./. (self-args // {
6669 system = "x86_64-darwin";
6770 iosSdkVersion = "10.2";
skeleton/common/src/Common/Api.hs+1−1
@@ -1,4 +1,4 @@
11module Common.Api where
22
33commonStuff :: String
4−commonStuff = "Here is a string defined in code common to the frontend and backend."
4+commonStuff = "Here is a string defined in Common.Api"
skeleton/config/common/examplenew file+1
@@ -0,0 +1 @@
1+This string comes from config/common/example
skeleton/frontend/frontend.cabal+1
@@ -10,6 +10,7 @@ library
1010 , obelisk-frontend
1111 , obelisk-route
1212 , reflex-dom
13+ , obelisk-executable-config-lookup
1314 , obelisk-generated-static
1415 , text
1516 exposed-modules:
skeleton/frontend/src/Frontend.hs+8−1
@@ -4,7 +4,9 @@
44module Frontend where
55
66import qualified Data.Text as T
7+import qualified Data.Text.Encoding as T
78import Obelisk.Frontend
9+import Obelisk.Configs
810import Obelisk.Route
911import Reflex.Dom.Core
1012
@@ -12,7 +14,6 @@ import Common.Api
1214import Common.Route
1315import Obelisk.Generated.Static
1416
15−
1617frontend :: Frontend (R FrontendRoute)
1718frontend = Frontend
1819 { _frontend_head = el "title" $ text "Obelisk Minimal Example"
@@ -20,4 +21,10 @@ frontend = Frontend
2021 text "Welcome to Obelisk!"
2122 el "p" $ text $ T.pack commonStuff
2223 elAttr "img" ("src" =: static @"obelisk.jpg") blank
24+ el "div" $ do
25+ exampleConfig <- getConfig "common/example"
26+ case exampleConfig of
27+ Nothing -> text "No config file found in config/common/example"
28+ Just s -> text (T.decodeUtf8 s)
29+ return ()
2330 }