on master help sign in
Merge branch 'develop' into add-css-example
commit1f7c481914b920840c7f0918c5ad3a238ad2b58e authorAli Abrar <aliabrar@gmail.com> dateFri 4 Oct 2019, 12:10 (7 years ago) parentdf8e73eb85c9ee
31 files changed+472−195
CONTRIBUTING.md+5 ChangeLog.md+9 README.md+45−9 default.nix+18−13 dep/reflex-platform/github.json+3−3 lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+7−6 lib/backend/src/Obelisk/Backend.hs+14−17 lib/cliapp/src/Obelisk/CliApp/Demo.hs+2−1 lib/cliapp/src/Obelisk/CliApp/Process.hs+5−4 lib/cliapp/src/Obelisk/CliApp/Types.hs+7−3 lib/command/obelisk-command.cabal+1−1 lib/command/src/Obelisk/App.hs+5−2 lib/command/src/Obelisk/Command.hs+27−4 lib/command/src/Obelisk/Command/Deploy.hs+17−17 lib/command/src/Obelisk/Command/Project.hs+1 lib/command/src/Obelisk/Command/Run.hs+15−3 lib/command/src/Obelisk/Command/Thunk.hs+2−2 lib/command/src/Obelisk/Command/Utils.hs+1−1 lib/command/src/Obelisk/Command/VmBuilder.hs+2−1 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+17−8 lib/executable-config/lookup/src/Obelisk/Configs.hs+4 lib/frontend/src/Obelisk/Frontend.hs+22−14 lib/route/src/Obelisk/Route.hs+201−30 lib/run/src/Obelisk/Run.hs+11−12 lib/selftest/src/Obelisk/SelfTest.hs+12−15 release.nix+3−6 skeleton/backend/src/Backend.hs+1−3 skeleton/common/src/Common/Route.hs+12−18 skeleton/frontend/src-bin/main.hs+1−1 skeleton/frontend/src/Frontend.hs+1−1 skeleton/shell.nix+1
CONTRIBUTING.md+5
@@ -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
ChangeLog.md+9
@@ -3,12 +3,21 @@
33This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
44
55## Unreleased
6+
7+* Generalised pathSegmentEncoder, added pathFieldEncoder.
8+* Added some Prisms to the encoder library for manipulating DSums (perhaps they should get moved to dependent-sum before release?)
9+* Add "ob doc" command, which lists paths to haddock documentation for specified packages
10+* Bump reflex-platform so that obelisk now uses GHC 8.6.5 and the nixos-19.03 nixpkgs set
11+
12+## v0.2.0.0 - 2019-8-17
13+
614* Configs become ByteStrings.
715* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
816 * 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`.
917* The backend no longer runs in BackendConfigsT.
1018* Add tabulation package. See Data.Tabulation for details.
1119* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
20+* Use IP address for nginx proxy pass instead of localhost
1221
1322## v0.1.1.0 - 2019-05-17
1423
README.md+45−9
@@ -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)
@@ -59,6 +60,28 @@ Or to launch ghcid for `lib/command` project:
5960nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
6061```
6162
63+To re-install `ob` from source do
64+```
65+nix-env -f /path/to/obelisk -iA command
66+```
67+
68+Note that `ob` will defer to the version found in your project's `.obelisk/impl` directory. To update that version specifically:
69+
70+```shell
71+ob thunk unpack ./obelisk/impl
72+cd ./obelisk/impl
73+# apply your changes
74+```
75+
76+If you want to commit your changes, first push them to your fork of obelisk and then
77+
78+```shell
79+cd /your/project/root
80+ob thunk pack .obelisk/impl
81+git add .obelisk/impl
82+git commit -m "Bump obelisk"
83+```
84+
6285### Accessing private repositories
6386
6487To allow the Nix builder to access private git repositories, you must be set up
@@ -92,16 +115,20 @@ To enter a nix-shell from which you can run the hoogle command-line client or a
92115
93116`nix-shell -A shells.ghc --arg withHoogle true`
94117
118+### Adding packages
119+
120+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.
121+
95122### Adding package overrides
96123
97−To 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:
124+To 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 fetched from GitHub and a specific version of the `waargonaut` package fetched from Hackage, your `default.nix` will look like:
98125
99126```nix
100127# ...
101128project ./. ({ pkgs, ... }: {
102129# ...
103130 overrides = self: super: let
104− aeson = pkgs.fetchFromGitHub {
131+ aesonSrc = pkgs.fetchFromGitHub {
105132 owner = "obsidiansystems";
106133 repo = "aeson-gadt-th";
107134 rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
@@ -109,7 +136,12 @@ project ./. ({ pkgs, ... }: {
109136 };
110137 in
111138 {
112− aeson = self.callCabal2nix "aeson" aeson {};
139+ aeson = self.callCabal2nix "aeson" aesonSrc {};
140+ waargonaut = self.callHackageDirect {
141+ pkg = "waargonaut";
142+ ver = "0.8.0.1";
143+ sha256 = "1zv28np3k3hg378vqm89v802xr0g8cwk7gy3mr77xrzy5jbgpa39";
144+ } {};
113145 };
114146# ...
115147```
@@ -123,14 +155,16 @@ For further information see [the Haskell section](https://nixos.org/nixpkgs/manu
123155Build everything:
124156
125157```
126−nix-build -A exe -o result-exe
158+nix-build -A exe --no-out-link
127159```
128160
129−Run the server:
161+Copy the result to a new directory, add configuration, and run!
130162
131163```
132−cd result-exe
133−./backend
164+mkdir test-app
165+ln -s $(nix-build -A exe --no-out-link)/* test-app/
166+cp -r config test-app
167+(cd test-app && ./backend)
134168```
135169
136170### EC2
@@ -162,9 +196,11 @@ ob deploy init \
162196 ~/code/myapp-deploy
163197```
164198
165−NOTE: HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
199+HTTPS is enabled by default; to disable https, pass `--disable-https` to the `ob deploy init` command above.
200+
201+This step will also require that you manually verify the authenticity of the host `$SERVER`. Obelisk will save the fingerprint in a deployment-specific configuration. **Obelisk deployments do *not* rely on the `known_hosts` of your local machine.** This is because, in the event that you need to switch from one deploy machine / bastion host to another, you want to be absolutely sure that you're still connecting to the machines you think you are, even if that deploy machine / bastion host has never connected to them before. Obelisk explicitly avoids a workflow that encourages people to accept host keys without checking them, since that could result in leaking production secrets to anyone who manages to MITM you, e.g. via DNS spoofing or cache poisoning. (Note that an active attack is a circumstance where you may need to quickly switch bastion hosts, e.g. because the attacker has taken one down or you have taken it down in case it was compromised. In this circumstance you might need to deploy to production to fix an exploit or rotate keys, etc.) When you run `ob deploy` later it will rely on the saved verification in this step.
166202
167−Then go to that created deployment configuration directory, and initiate the deployment:
203+Next, go to the deployment directory that you just initialized and deploy!
168204
169205```
170206cd ~/code/myapp-deploy
default.nix+18−13
@@ -32,13 +32,13 @@ let
3232 # Fix misc upstream packages
3333 (self: super: let
3434 pkgs = self.callPackage ({ pkgs }: pkgs) {};
35+ haskellLib = pkgs.haskell.lib;
3536 in {
36− hnix = pkgs.haskell.lib.dontCheck (self.callCabal2nix "hnix" (pkgs.fetchFromGitHub {
37− owner = "haskell-nix";
38− repo = "hnix";
39− rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
40− sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
41− }) {});
37+ hnix = haskellLib.overrideCabal (self.callHackage "hnix" "0.6.1" { these = self.these_0_8; }) (_: {
38+ jailbreak = true;
39+ doCheck = false;
40+ });
41+ hnix-store-core = self.callHackage "hnix-store-core" "0.1.0.0" {};
4242 })
4343
4444 pkgs.obeliskExecutableConfig.haskellOverlay
@@ -128,7 +128,7 @@ in rec {
128128 obelisk = ghcObelisk;
129129 obeliskEnvs = ghcObeliskEnvs;
130130 command = ghcObelisk.obelisk-command;
131− shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
131+ shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
132132
133133 selftest = pkgs.writeScript "selftest" ''
134134 #!/usr/bin/env bash
@@ -157,12 +157,16 @@ in rec {
157157 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
158158 '';
159159
160− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
160+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
161161 mkdir $out
162162 cd $out
163163 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
164− closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
165− echo "//# sourceMappingURL=all.js.map" >> all.js
164+ ${if optimizationLevel == null then ''
165+ ln -s all.unminified.js all.js
166+ '' else ''
167+ ${pkgs.closurecompiler}/bin/closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ${optimizationLevel} --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
168+ echo "//# sourceMappingURL=all.js.map" >> all.js
169+ ''}
166170 '';
167171
168172 serverModules = {
@@ -198,7 +202,7 @@ in rec {
198202 enableACME = enableHttps;
199203 forceSSL = enableHttps;
200204 locations.${baseUrl} = {
201− proxyPass = "http://localhost:" + toString internalPort;
205+ proxyPass = "http://127.0.0.1:" + toString internalPort;
202206 proxyWebsockets = true;
203207 };
204208 };
@@ -207,10 +211,11 @@ in rec {
207211 wantedBy = [ "multi-user.target" ];
208212 after = [ "network.target" ];
209213 restartIfChanged = true;
214+ path = [ pkgs.gnutar ];
210215 script = ''
211216 ln -sft . '${exe}'/*
212217 mkdir -p log
213− exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
218+ exec ./backend ${backendArgs} </dev/null
214219 '';
215220 serviceConfig = {
216221 User = user;
@@ -268,7 +273,7 @@ in rec {
268273 , tools ? _: []
269274 , shellToolOverrides ? _: _: {}
270275 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
271− , __closureCompilerOptimizationLevel ? "ADVANCED"
276+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
272277 }:
273278 let frontendName = "frontend";
274279 backendName = "backend";
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "cg-bump-reflex-dom",
5− "rev": "7ecb4c255bb053a7a4796ffd6331cdbe210de91c",
6− "sha256": "1mngxa24cfpvxxq4hgh77nw36vny4abl5wi2xmlwpkk25wzm0h0x"
4+ "branch": "develop",
5+ "rev": "9e306f72ed0dbcdccce30a4ba0eb37aa03cf91e3",
6+ "sha256": "1crlwfw6zsx2izwsd57i5651p5pfyrpkfjwp6fc1yd4q4d4n7g2m"
77}
lib/asset/serve-snap/src/Obelisk/Asset/Serve/Snap.hs+7−6
@@ -19,6 +19,7 @@ import Control.Applicative ((<|>))
1919import Control.Exception (handleJust, try, throwIO)
2020import Control.Monad (forM, liftM)
2121import Control.Monad.IO.Class (liftIO)
22+import Control.Monad.Fail (MonadFail)
2223import Data.Attoparsec.ByteString (parseOnly, endOfInput)
2324import Data.ByteString (ByteString)
2425import qualified Data.ByteString as BS
@@ -38,7 +39,7 @@ import System.Posix (getFileStatus, fileSize)
3839-- current version of the asset
3940-- (e.g. @0rx5yvkkgkig2pcqf4ngi1l7vh89qqajdzc6aiayaibyhvj0d853-test.png@). Hashed
4041-- asset names will be sent with permanent caching headers.
41−serveAssets :: MonadSnap m => FilePath -> FilePath -> m ()
42+serveAssets :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
4243serveAssets = serveAssets' True
4344
4445-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -47,15 +48,15 @@ serveAssets = serveAssets' True
4748-- browser for the logical asset name (e.g. @test.png@) but instead the current
4849-- version of the asset with to point the browser at the unique filename for the
4950-- current version of the asset.
50−serveAssetsInPlace :: MonadSnap m => FilePath -> FilePath -> m ()
51+serveAssetsInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> m ()
5152serveAssetsInPlace = serveAssets' False
5253
5354-- | Like 'serveAssets', but only serves a single specified asset
54−serveAsset :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
55+serveAsset :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
5556serveAsset = serveAsset' True
5657
5758-- | Like 'serveAssetsInPlace', but only serves a single specified asset
58−serveAssetInPlace :: MonadSnap m => FilePath -> FilePath -> FilePath -> m ()
59+serveAssetInPlace :: (MonadSnap m, MonadFail m) => FilePath -> FilePath -> FilePath -> m ()
5960serveAssetInPlace = serveAsset' False
6061
6162-- | Serve static assets from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
@@ -63,13 +64,13 @@ serveAssetInPlace = serveAsset' False
6364-- For assets generated from @assets.nix@, the @Bool@ argument @doRedirect@ controls whether redirects will be sent to the browser if a request is made for
6465-- an unhashed asset name, e.g. @test.png@. For @True@, a redirect will be sent, yielding more round trips to the server but better caching behavior if the
6566-- asset doesn't change often. Conversely for @False@, the asset will be served "in place" but made uncacheable.
66−serveAssets' :: MonadSnap m => Bool -> FilePath -> FilePath -> m ()
67+serveAssets' :: (MonadSnap m, MonadFail m) => Bool -> FilePath -> FilePath -> m ()
6768serveAssets' doRedirect base fallback = do
6869 pRaw <- getSafePath
6970 serveAsset' doRedirect base fallback $ if "/" `isSuffixOf` pRaw || pRaw == "" then pRaw <> "index.html" else pRaw
7071
7172-- | Serve a single static asset from an asset directory generated via @assets.nix@ or, failing that, from a regular directory.
72−serveAsset' :: MonadSnap m => Bool -> FilePath -> FilePath -> FilePath -> m ()
73+serveAsset' :: (MonadFail m, MonadSnap m) => Bool -> FilePath -> FilePath -> FilePath -> m ()
7374serveAsset' doRedirect base fallback p = do
7475 assetType <- liftIO $ try $ BS.readFile $ base </> p </> "type"
7576 case assetType of
lib/backend/src/Obelisk/Backend.hs+14−17
@@ -26,12 +26,12 @@ import Prelude hiding (id, (.))
2626import Control.Category
2727import Control.Monad
2828import Control.Monad.Except
29+import Control.Monad.Fail (MonadFail)
2930import Control.Categorical.Bifunctor
3031import Data.ByteString (ByteString)
3132import qualified Data.ByteString.Char8 as BSC8
3233import Data.Default (Default (..))
3334import Data.Dependent.Sum
34−import Data.Functor.Sum
3535import Data.Functor.Identity
3636import Data.Map (Map)
3737import qualified Data.Map as Map
@@ -47,12 +47,12 @@ import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
4747import Reflex.Dom
4848import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, modifyResponse
4949 , rqPathInfo, rqQueryString, setContentType, writeBS, writeText
50− , rqCookies, Cookie(..))
50+ , rqCookies, Cookie(..) , setHeader)
5151import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
5252import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
5353
5454data Backend backendRoute frontendRoute = Backend
55− { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
55+ { _backend_routeEncoder :: Encoder (Either Text) Identity (R (FullRoute backendRoute frontendRoute)) PageName
5656 , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
5757 }
5858
@@ -65,7 +65,7 @@ data GhcjsApp route = GhcjsApp
6565-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
6666--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
6767serveDefaultObeliskApp
68− :: (MonadSnap m, HasCookies m)
68+ :: (MonadSnap m, HasCookies m, MonadFail m)
6969 => (R appRoute
7070 -> Text)
7171 -> ([Text]
@@ -114,11 +114,7 @@ getPageName :: (MonadSnap m) => m PageName
114114getPageName = do
115115 p <- getsRequest rqPathInfo
116116 q <- getsRequest rqQueryString
117− let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
118− pageNameEncoder' = bimap
119− (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
120− (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
121− return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
117+ return $ byteStringsToPageName p q
122118
123119getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
124120getRouteWith e = do
@@ -126,7 +122,7 @@ getRouteWith e = do
126122 return $ tryDecode e pageName
127123
128124serveObeliskApp
129− :: (MonadSnap m, HasCookies m)
125+ :: (MonadSnap m, HasCookies m, MonadFail m)
130126 => (R appRoute -> Text)
131127 -> ([Text] -> m ())
132128 -> GhcjsApp (R appRoute)
@@ -144,7 +140,7 @@ serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
144140 writeText msg
145141 ResourceRoute_Version :=> Identity () -> doNotCache >> serveFileIfExistsAs "text/plain" "version"
146142
147−serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
143+serveStaticAssets :: (MonadSnap m, MonadFail m) => StaticAssets -> [Text] -> m ()
148144serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
149145
150146data StaticAssets = StaticAssets
@@ -162,7 +158,7 @@ staticRenderContentType = "text/html; charset=utf-8"
162158
163159--TODO: Don't assume we're being served at "/"
164160serveGhcjsApp
165− :: (MonadSnap m, HasCookies m)
161+ :: (MonadSnap m, HasCookies m, MonadFail m)
166162 => (R appRouteComponent -> Text)
167163 -> GhcjsApp (R appRouteComponent)
168164 -> Map Text ByteString
@@ -171,10 +167,11 @@ serveGhcjsApp
171167serveGhcjsApp urlEnc app config = \case
172168 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
173169 modifyResponse $ setContentType staticRenderContentType
170+ modifyResponse $ setHeader "Cache-Control" "no-store private"
174171 writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
175172 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
176173
177−runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
174+runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
178175runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
179176 Left e -> fail $ "backend error:\n" <> T.unpack e
180177 Right validFullEncoder -> do
@@ -183,15 +180,15 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
183180 runSnapWithCommandLineArgs $ do
184181 getRouteWith validFullEncoder >>= \case
185182 Identity r -> case r of
186− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
187− InR obeliskRoute :=> Identity a ->
183+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
184+ FullRoute_Frontend obeliskRoute :/ a ->
188185 serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
189186 obeliskRoute :/ a
190187
191−mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
188+mkRouteToUrl :: Encoder Identity parse (R (FullRoute br fr)) PageName -> R fr -> Text
192189mkRouteToUrl validFullEncoder =
193190 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
194− in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
191+ in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (FullRoute_Frontend $ ObeliskRoute_App k) :/ v
195192
196193renderGhcjsFrontend
197194 :: (MonadSnap m, HasCookies m)
lib/cliapp/src/Obelisk/CliApp/Demo.hs+2−1
@@ -4,6 +4,7 @@
44module Obelisk.CliApp.Demo where
55
66import Control.Concurrent (threadDelay)
7+import Control.Monad.Fail
78import Control.Monad.IO.Class
89import Data.Semigroup ((<>))
910import qualified Data.Text as T
@@ -14,7 +15,7 @@ import Control.Monad.Catch (MonadMask)
1415import Obelisk.CliApp
1516
1617cliDemo
17− :: ( MonadIO m, MonadMask m
18+ :: ( MonadIO m, MonadMask m, MonadFail m
1819 , CliLog m, HasCliConfig e m, CliThrow e m, AsProcessFailure e, AsUnstructuredError e)
1920 => m ()
2021cliDemo = withSpinner "CLI Demo" $ do
lib/cliapp/src/Obelisk/CliApp/Process.hs+5−4
@@ -24,6 +24,7 @@ module Obelisk.CliApp.Process
2424
2525import Control.Monad ((<=<), join, void)
2626import Control.Monad.Except (throwError)
27+import Control.Monad.Fail
2728import Control.Monad.IO.Class (MonadIO, liftIO)
2829import Control.Lens (Prism', review)
2930import qualified Data.ByteString as BS
@@ -60,7 +61,7 @@ instance AsProcessFailure ProcessFailure where
6061 asProcessFailure = id
6162
6263readProcessAndLogStderr
63− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
64+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
6465 => Severity -> CreateProcess -> m Text
6566readProcessAndLogStderr sev process = do
6667 (out, _err) <- withProcess process $ \_out err -> do
@@ -82,7 +83,7 @@ readCreateProcessWithExitCode process = do
8283-- which case it is advisable to call it with a non-Error severity for stderr, like
8384-- `callProcessAndLogOutput (Debug, Debug)`.
8485readProcessAndLogOutput
85− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
86+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
8687 => (Severity, Severity) -> CreateProcess -> m Text
8788readProcessAndLogOutput (sev_out, sev_err) process = do
8889 (_, Just out, Just err, p) <- createProcess $ process
@@ -106,7 +107,7 @@ readProcessAndLogOutput (sev_out, sev_err) process = do
106107-- `callProcessAndLogOutput (Debug, Debug)`.
107108callProcessAndLogOutput
108109
109− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
110+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
110111 => (Severity, Severity) -> CreateProcess -> m ()
111112callProcessAndLogOutput (sev_out, sev_err) process =
112113 void $ withProcess process $ \out err -> do
@@ -150,7 +151,7 @@ callCommand cmd = do
150151 liftIO $ Process.callCommand cmd
151152
152153withProcess
153− :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e)
154+ :: (MonadIO m, CliLog m, CliThrow e m, AsProcessFailure e, MonadFail m)
154155 => CreateProcess -> (Handle -> Handle -> m ()) -> m (Handle, Handle)
155156withProcess process f = do -- TODO: Use bracket.
156157 -- FIXME: Using `withCreateProcess` here leads to something operating illegally on closed handles.
lib/cliapp/src/Obelisk/CliApp/Types.hs+7−3
@@ -6,12 +6,14 @@
66{-# LANGUAGE GeneralizedNewtypeDeriving #-}
77{-# LANGUAGE MultiParamTypeClasses #-}
88{-# LANGUAGE OverloadedStrings #-}
9+{-# LANGUAGE StandaloneDeriving #-}
910{-# LANGUAGE UndecidableInstances #-}
1011module Obelisk.CliApp.Types where
1112
1213import Control.Concurrent.MVar (MVar)
1314import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
14−import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
15+import Control.Monad.Fail (MonadFail)
16+import Control.Monad.Log (LoggingT(..), MonadLog, Severity (..), WithSeverity (..), logMessage)
1517import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader (..), ask, mapReaderT)
1618import Control.Monad.Writer (WriterT)
1719import Control.Monad.State (StateT)
@@ -45,9 +47,11 @@ type CliThrow e m = MonadError e m
4547putLog :: CliLog m => Severity -> Text -> m ()
4648putLog sev = logMessage . Output_Log . WithSeverity sev
4749
50+deriving instance MonadFail m => MonadFail (LoggingT Output m)
51+
4852newtype DieT e m a = DieT { unDieT :: ReaderT (e -> (Text, Int)) (LoggingT Output m) a }
4953 deriving
50− ( Functor, Applicative, Monad, MonadIO
54+ ( Functor, Applicative, Monad, MonadIO, MonadFail
5155 , MonadThrow, MonadCatch, MonadMask
5256 , MonadLog Output
5357 )
@@ -114,7 +118,7 @@ newtype CliT e m a = CliT
114118 { unCliT :: ReaderT (CliConfig e) (DieT e m) a
115119 }
116120 deriving
117− ( Functor, Applicative, Monad, MonadIO
121+ ( Functor, Applicative, Monad, MonadIO, MonadFail
118122 , MonadThrow, MonadCatch, MonadMask
119123 , MonadLog Output -- CliLog
120124 , MonadError e -- CliThrow
lib/command/obelisk-command.cabal+1−1
@@ -23,7 +23,7 @@ library
2323 , filepath
2424 , github
2525 , here
26− , hit
26+ , git
2727 , hnix
2828 , hpack
2929 , io-streams
lib/command/src/Obelisk/App.hs+5−2
@@ -12,6 +12,7 @@ module Obelisk.App where
1212
1313import Control.Lens
1414import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
15+import Control.Monad.Fail (MonadFail)
1516import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1617import Control.Monad.Writer (WriterT)
1718import Control.Monad.State (StateT)
@@ -61,10 +62,11 @@ newtype ObeliskT m a = ObeliskT
6162 { unObeliskT :: ReaderT Obelisk (CliT ObeliskError m) a
6263 }
6364 deriving
64− ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
65+ ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask, MonadFail
6566 , MonadLog Output -- CliLog
6667 , MonadError ObeliskError -- CliThrow ObeliskError
67− , HasCliConfig ObeliskError)
68+ , HasCliConfig ObeliskError
69+ )
6870
6971instance MonadTrans ObeliskT where
7072 lift = ObeliskT . lift . lift
@@ -104,6 +106,7 @@ type MonadInfallibleObelisk m =
104106type MonadObelisk m =
105107 ( MonadInfallibleObelisk m
106108 , CliThrow ObeliskError m
109+ , MonadFail m
107110 )
108111
109112getObeliskUserStateDir :: IO FilePath
lib/command/src/Obelisk/Command.hs+27−4
@@ -20,6 +20,7 @@ import Data.Text.Encoding
2020import Data.Text.Encoding.Error (lenientDecode)
2121import GHC.StaticPtr
2222import Options.Applicative
23+import Options.Applicative.Help.Pretty (text, (<$$>))
2324import System.Directory
2425import System.Environment
2526import System.FilePath
@@ -93,6 +94,7 @@ data ObCommand
9394 | ObCommand_Repl
9495 | ObCommand_Watch
9596 | ObCommand_Shell ShellOpts
97+ | ObCommand_Doc String [String] -- shell and list of packages
9698 | ObCommand_Internal ObInternal
9799 deriving Show
98100
@@ -129,6 +131,11 @@ obCommand cfg = hsubparser
129131 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
130132 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
131133 , command "shell" $ info (ObCommand_Shell <$> shellOpts) $ progDesc "Enter a shell with project dependencies"
134+ , command "doc" $ info (ObCommand_Doc <$> shellFlags <*> packageNames) $
135+ progDesc "List paths to haddock documentation for specified packages"
136+ <> footerDoc (Just $
137+ text "Hint: To open the documentation you can pipe the output of this command like"
138+ <$$> text "ob doc reflex reflex-dom-core | xargs -n1 xdg-open")
132139 ])
133140 <|> subparser
134141 (mconcat
@@ -136,6 +143,9 @@ obCommand cfg = hsubparser
136143 , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
137144 ])
138145
146+packageNames :: Parser [String]
147+packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
148+
139149deployCommand :: ArgsConfig -> Parser DeployCommand
140150deployCommand cfg = hsubparser $ mconcat
141151 [ command "init" $ info (DeployCommand_Init <$> deployInitOpts) $ progDesc "Initialize a deployment configuration directory"
@@ -233,12 +243,15 @@ data ShellOpts
233243 }
234244 deriving Show
235245
246+shellFlags :: Parser String
247+shellFlags =
248+ flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
249+ <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
250+ <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
251+
236252shellOpts :: Parser ShellOpts
237253shellOpts = ShellOpts
238− <$> ( flag' "ghc" (long "ghc" <> help "Enter a shell environment having ghc (default)")
239− <|> flag "ghc" "ghcjs" (long "ghcjs" <> help "Enter a shell having ghcjs rather than ghc")
240− <|> strOption (short 'A' <> long "argument" <> metavar "NIXARG" <> help "Use the environment specified by the given nix argument of `shells'")
241− )
254+ <$> shellFlags
242255 <*> optional (strArgument (metavar "COMMAND"))
243256
244257parserPrefs :: ParserPrefs
@@ -369,6 +382,8 @@ ob = \case
369382 ObCommand_Watch -> inNixShell' $ static runWatch
370383 ObCommand_Shell so -> withProjectRoot "." $ \root ->
371384 projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
385+ ObCommand_Doc shell pkgs -> withProjectRoot "." $ \root ->
386+ projectShell root False shell (Just $ haddockCommand pkgs)
372387 ObCommand_Internal icmd -> case icmd of
373388 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
374389 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
@@ -377,6 +392,14 @@ ob = \case
377392 liftIO $ runObelisk c $ deRefStaticPtr p
378393 ObInternal_CLIDemo -> cliDemo
379394
395+haddockCommand :: [String] -> String
396+haddockCommand pkgs = unwords
397+ [ "for p in"
398+ , unwords [getHaddockPath p ++ "/index.html" | p <- pkgs]
399+ , "; do echo $p; done"
400+ ]
401+ where getHaddockPath p = "$(ghc-pkg field " ++ p ++ " haddock-html --simple-output)"
402+
380403--TODO: Clean up all the magic strings throughout this codebase
381404
382405getArgsConfig :: IO ArgsConfig
lib/command/src/Obelisk/Command/Deploy.hs+17−17
@@ -4,7 +4,6 @@
44{-# LANGUAGE ScopedTypeVariables #-}
55{-# LANGUAGE ViewPatterns #-}
66{-# LANGUAGE DeriveGeneric #-}
7−{-# LANGUAGE TypeApplications #-}
87module Obelisk.Command.Deploy where
98
109import Control.Lens
@@ -22,8 +21,8 @@ import qualified Data.Set as Set
2221import qualified Data.Text as T
2322import qualified Data.Text.IO as T
2423import GHC.Generics
25−import Nix.Convert
2624import Nix.Pretty
25+import Nix.String (principledMakeNixStringWithoutContext)
2726import Nix.Value
2827import System.Directory
2928import System.Environment (getEnvironment)
@@ -173,13 +172,14 @@ deployPush deployPath getNixBuilders = do
173172deployUpdate :: MonadObelisk m => FilePath -> m ()
174173deployUpdate deployPath = updateThunkToLatest (deployPath </> "src") Nothing
175174
176−keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValueNF Identity)
177−keytoolToAndroidConfig conf = runIdentity $ do
178− path <- toValue $ Path $ _keytoolConfig_keystore conf
179− storepass <- toValue $ T.pack $ _keytoolConfig_storepass conf
180− alias <- toValue $ T.pack $ _keytoolConfig_alias conf
181− keypass <- toValue $ T.pack $ _keytoolConfig_keypass conf
182− return $ HM.fromList
175+keytoolToAndroidConfig :: KeytoolConfig -> HM.HashMap Text (NValue t Identity m)
176+keytoolToAndroidConfig conf =
177+ let path = nvPath $ _keytoolConfig_keystore conf
178+ fromStr = nvStr . principledMakeNixStringWithoutContext . T.pack
179+ storepass = fromStr $ _keytoolConfig_storepass conf
180+ alias = fromStr $ _keytoolConfig_alias conf
181+ keypass = fromStr $ _keytoolConfig_keypass conf
182+ in HM.fromList
183183 [ ("storeFile", path)
184184 , ("storePassword", storepass)
185185 , ("keyAlias", alias)
@@ -194,7 +194,7 @@ renderPlatformDeployment = \case
194194 Android -> "android"
195195 IOS -> "ios"
196196
197−deployMobile :: MonadObelisk m => PlatformDeployment -> [String] -> m ()
197+deployMobile :: forall m. MonadObelisk m => PlatformDeployment -> [String] -> m ()
198198deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
199199 let srcDir = root </> "src"
200200 configDir = root </> "config"
@@ -224,12 +224,12 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
224224 checkKeytoolConfExist <- liftIO $ doesFileExist keytoolConfPath
225225 unless checkKeytoolConfExist $ failWith "Missing android KeytoolConfig"
226226 keytoolConfContents <- liftIO $ BSL.readFile keytoolConfPath
227− liftIO $ putStrLn $ show keytoolConfContents
228− releaseKey <- case eitherDecode keytoolConfContents of
227+ liftIO $ print keytoolConfContents
228+ releaseKey :: String <- case eitherDecode keytoolConfContents :: Either String KeytoolConfig of
229229 Left err -> failWith $ T.pack err
230− Right conf -> do
231− let nvset = toValue @(HM.HashMap Text (NValueNF Identity)) @Identity @(NValueNF Identity) $ keytoolToAndroidConfig conf
232− return $ printNix $ runIdentity nvset
230+ Right conf -> return $
231+ (printNix :: MonadObelisk m => NValue t Identity m -> String) $
232+ nvSet (keytoolToAndroidConfig conf) HM.empty
233233 let expr = mconcat
234234 [ "with (import ", srcDir, " {});"
235235 , "android.frontend.override (drv: { "
@@ -255,7 +255,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
255255 result <- nixCmd $ NixCmd_Build $ def
256256 & nixBuildConfig_outLink .~ OutLink_None
257257 & nixCmdConfig_target .~ nixBuildTarget
258− putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> (show result)
258+ putLog Notice $ T.pack $ "Your recently built android apk can be found at the following path: " <> show result
259259 callProcessAndLogOutput (Notice, Error) $ proc (result </> "bin" </> "deploy") mobileArgs
260260 where
261261 withEcho showEcho f = do
@@ -275,7 +275,7 @@ instance ToJSON KeytoolConfig
275275createKeystore :: MonadObelisk m => FilePath -> KeytoolConfig -> m ()
276276createKeystore root config = do
277277 let expr = "with (import " <> toImplDir root <> ").reflex-platform.nixpkgs; pkgs.mkShell { buildInputs = [ pkgs.jdk ]; }"
278− callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
278+ callProcessAndLogOutput (Notice,Notice) $ (proc "nix-shell" ["default.nix", "-E" , expr, "--run" , keytoolCmd]) { cwd = Just root }
279279 where
280280 keytoolCmd = processToShellString "keytool"
281281 [ "-genkeypair", "-noprompt"
lib/command/src/Obelisk/Command/Project.hs+1
@@ -205,6 +205,7 @@ inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
205205projectShell :: MonadObelisk m => FilePath -> Bool -> String -> Maybe String -> m ()
206206projectShell root isPure shellName command = do
207207 (_, _, _, ph) <- createProcess_ "runNixShellAttr" $ setCtlc $ setCwd (Just root) $ proc "nix-shell" $
208+ [ "default.nix"] <>
208209 [ "--pure" | isPure ] <>
209210 [ "-A"
210211 , "shells." <> shellName
lib/command/src/Obelisk/Command/Run.hs+15−3
@@ -14,6 +14,7 @@ import qualified Data.List as L
1414import qualified Data.List.NonEmpty as NE
1515import Data.Maybe
1616import qualified Data.Text as T
17+import Distribution.Compiler (CompilerFlavor(..))
1718import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
1819import Distribution.Parsec.ParseResult (runParseResult)
1920import Distribution.Pretty
@@ -48,6 +49,8 @@ data CabalPackageInfo = CabalPackageInfo
4849 -- ^ List of globally enable extensions of the library component
4950 , _cabalPackageInfo_defaultLanguage :: Maybe Language
5051 -- ^ List of globally set languages of the library component
52+ , _cabalPackageInfo_compilerOptions :: [(CompilerFlavor, [String])]
53+ -- ^ List of compiler-specific options (e.g., the "ghc-options" field of the cabal file)
5154 }
5255
5356-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
@@ -73,7 +76,10 @@ run = do
7376 -- Check whether the impure static files are a derivation (and so must be built)
7477 if isDerivation == "1"
7578 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"]
79+ proc "nix-build"
80+ [ "--no-out-link"
81+ , "-E", "(import " <> importableRoot <> "{}).passthru.staticFilesImpure"
82+ ]
7783 else readProcessAndLogStderr Debug $
7884 proc "nix" ["eval", "-f", root, "passthru.staticFilesImpure", "--raw"]
7985 putLog Debug $ "Assets impurely loaded from: " <> assets
@@ -115,12 +121,12 @@ parseCabalPackage dir = do
115121 then Just <$> liftIO (readUTF8File cabalFp)
116122 else if hasHpack
117123 then do
118− let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
124+ let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
119125 liftIO (readPackageConfig decodeOptions) >>= \case
120126 Left err -> do
121127 putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
122128 return Nothing
123− Right (DecodeResult hpackPackage _ _) -> do
129+ Right (DecodeResult hpackPackage _ _ _) -> do
124130 return $ Just $ renderPackage [] hpackPackage
125131 else return Nothing
126132
@@ -140,6 +146,7 @@ parseCabalPackage dir = do
140146 defaultExtensions $ libBuildInfo lib
141147 , _cabalPackageInfo_defaultLanguage =
142148 defaultLanguage $ libBuildInfo lib
149+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
143150 }
144151 Left (_, errors) -> do
145152 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -175,8 +182,13 @@ withGhciScript pkgs f = do
175182 extensionsLine = if extensions == mempty
176183 then ""
177184 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
185+ ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
186+ packageInfos >>= _cabalPackageInfo_compilerOptions
178187 dotGhci = unlines $
179188 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
189+ , case ghcOptions of
190+ [] -> ""
191+ xs -> ":set " <> intercalate " " xs
180192 , extensionsLine
181193 , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
182194 , ":load Backend Frontend"
lib/command/src/Obelisk/Command/Thunk.hs+2−2
@@ -94,7 +94,7 @@ type NixSha256 = Text --TODO: Use a smart constructor and make this actually ver
9494
9595-- | A specific revision of data; it may be available from multiple sources
9696data ThunkRev = ThunkRev
97− { _thunkRev_commit :: Ref
97+ { _thunkRev_commit :: Ref Ref.SHA1
9898 , _thunkRev_nixSha256 :: NixSha256
9999 }
100100 deriving (Show, Eq, Ord)
@@ -149,7 +149,7 @@ getThunkGitBranch (ThunkPtr _ src) = fmap untagName $ case src of
149149 ThunkSource_GitHub s -> _gitHubSource_branch s
150150 ThunkSource_Git s -> _gitSource_branch s
151151
152−commitNameToRef :: Name Commit -> Ref
152+commitNameToRef :: Name Commit -> Ref Ref.SHA1
153153commitNameToRef (N c) = Ref.fromHex $ encodeUtf8 c
154154
155155-- TODO: Use spinner here.
lib/command/src/Obelisk/Command/Utils.hs+1−1
@@ -134,7 +134,7 @@ gitLsRemote repository mRef mBranch = do
134134 ["ls-remote", "--exit-code", repository, branchName]
135135 let t = T.pack out
136136 maps <- case MP.runParser parseLsRemote "" t of
137− Left err -> failWith $ T.pack $ MP.parseErrorPretty' t err
137+ Left err -> failWith $ T.pack $ MP.errorBundlePretty err
138138 Right table -> pure $ bimap M.fromList M.fromList $ partitionEithers $ table
139139 putLog Debug $ "git ls-remote maps: " <> T.pack (show maps)
140140 pure (exitCode, maps)
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/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+17−8
@@ -15,28 +15,37 @@ import GHCJS.DOM.Document (getHead)
1515import GHCJS.DOM.DOMStringMap (get)
1616import GHCJS.DOM.Element (getInnerHTML)
1717import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
18−import GHCJS.DOM.NodeList (item, getLength)
18+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
1919import GHCJS.DOM.ParentNode (querySelectorAll)
20−import GHCJS.DOM.Types (Node(Node), castTo)
20+import GHCJS.DOM.Types (MonadJSM, Node(Node), castTo)
2121
2222getConfigs :: IO (Map Text ByteString)
2323getConfigs = do
2424 Just doc <- currentDocument
2525 Just hd <- getHead doc
26− nodes <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
26+ nodes <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
2727 fmap Map.fromList $ for nodes $ \node -> do
2828 e <- castTo HTMLElement node >>= \case
2929 Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
3030 Just htmlE -> return htmlE
3131 dataset <- getDataset e
3232 (,)
33− <$> get dataset ("obelisk-executable-config-inject-key" :: Text)
33+ -- the key is camelCased: https://html.spec.whatwg.org/multipage/dom.html#dom-dataset
34+ <$> get dataset ("obeliskExecutableConfigInjectKey" :: Text)
3435 <*> (fmap decodeOrFail (getInnerHTML e))
3536 where
36− collToList es = do
37− len <- getLength es
38− list <- traverse (item es) [0..len-1]
39− pure $ catMaybes list
4037 decodeOrFail x = case B64.decode (T.encodeUtf8 x) of
4138 Left e -> error ("Obelisk.ExecutableConfig.Lookup.getConfigs: error when decoding base64: " ++ e)
4239 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/Obelisk/Configs.hs+4
@@ -31,6 +31,7 @@ import Data.ByteString (ByteString)
3131import Data.Map (Map)
3232import qualified Data.Map as Map
3333import Data.Text (Text)
34+import qualified Data.Text.Encoding as T
3435import Reflex
3536import Reflex.Host.Class
3637import Reflex.Dom
@@ -50,6 +51,9 @@ class Monad m => HasConfigs m where
5051instance Monad m => HasConfigs (ConfigsT m) where
5152 getConfigs = ConfigsT ask
5253
54+getTextConfig :: HasConfigs m => Text -> m (Maybe Text)
55+getTextConfig k = fmap T.decodeUtf8 <$> getConfig k
56+
5357instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
5458instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
5559instance HasConfigs m => HasConfigs (EventWriterT t w m)
lib/frontend/src/Obelisk/Frontend.hs+22−14
@@ -43,11 +43,11 @@ import qualified GHCJS.DOM as DOM
4343import qualified GHCJS.DOM.Types as DOM
4444import qualified GHCJS.DOM.History as DOM
4545import qualified GHCJS.DOM.Window as DOM
46−import Language.Javascript.JSaddle (JSM, jsNull)
46+import Language.Javascript.JSaddle (MonadJSM, JSM, jsNull)
4747import GHCJS.DOM (currentDocument)
4848import GHCJS.DOM.Document (getHead)
49−import GHCJS.DOM.Node (removeChild_)
50−import GHCJS.DOM.NodeList (item, getLength)
49+import GHCJS.DOM.Node (Node, removeChild_)
50+import GHCJS.DOM.NodeList (IsNodeList, item, getLength)
5151import GHCJS.DOM.ParentNode (querySelectorAll)
5252import Obelisk.Frontend.Cookie
5353import Obelisk.Route.Frontend
@@ -83,6 +83,7 @@ type ObeliskWidget js t route m =
8383 , PrebuildAgnostic t route (Client m)
8484 , HasConfigs m
8585 , HasCookies m
86+ , MonadIO (Performable m)
8687 )
8788
8889type PrebuildAgnostic t route m =
@@ -105,13 +106,20 @@ removeHTMLConfigs :: JSM ()
105106removeHTMLConfigs = void $ runMaybeT $ do
106107 doc <- MaybeT currentDocument
107108 hd <- MaybeT $ getHead doc
108− es <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
109+ es <- nodeListNodes =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
109110 for_ es $ removeChild_ hd
110− where
111− collToList es = do
112− len <- getLength es
113− lst <- traverse (item es) $ take (fromIntegral len) $ [0..] -- fun with unsigned types ...
114− pure $ catMaybes lst
111+
112+-- | Collect all nodes in the node list.
113+--
114+-- TODO: this and the version in exe-config/ghcjs/lookup should be
115+-- upstreamed to jsaddle.
116+nodeListNodes :: (IsNodeList l, MonadJSM m) => l -> m [Node]
117+nodeListNodes es = do
118+ len <- getLength es
119+ -- Warning! len is unsigned. If the NodeList is empty, we must avoid
120+ -- accidentally traversing over [0..maxBound::Word]
121+ nodes <- traverse (item es) $ if len == 0 then [] else [0..len-1]
122+ pure $ catMaybes nodes
115123
116124setInitialRoute :: Bool -> JSM ()
117125setInitialRoute useHash = do
@@ -137,7 +145,7 @@ data FrontendMode = FrontendMode
137145-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
138146runFrontend
139147 :: forall backendRoute route
140− . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
148+ . Encoder Identity Identity (R (FullRoute backendRoute route)) PageName
141149 -> Frontend (R route)
142150 -> JSM ()
143151runFrontend validFullEncoder frontend = do
@@ -165,14 +173,14 @@ runFrontend validFullEncoder frontend = do
165173 runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
166174
167175runFrontendWithConfigsAndCurrentRoute
168− :: forall backendRoute route
176+ :: forall backendRoute frontendRoute
169177 . FrontendMode
170178 -> Map Text ByteString
171− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
172− -> Frontend (R route)
179+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
180+ -> Frontend (R frontendRoute)
173181 -> JSM ()
174182runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
175− let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
183+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
176184 errorLeft = \case
177185 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
178186 Right x -> Identity x
lib/route/src/Obelisk/Route.hs+201−30
@@ -12,6 +12,7 @@
1212{-# LANGUAGE PatternSynonyms #-}
1313{-# LANGUAGE RankNTypes #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE StandaloneDeriving #-}
1516{-# LANGUAGE TemplateHaskell #-}
1617{-# LANGUAGE TypeApplications #-}
1718{-# LANGUAGE TypeFamilies #-}
@@ -62,10 +63,15 @@ module Obelisk.Route
6263 , shadowEncoder
6364 , prismEncoder
6465 , rPrism
66+ , _R
6567 , obeliskRouteEncoder
6668 , obeliskRouteSegment
6769 , pageNameEncoder
6870 , handleEncoder
71+ , FullRoute (..)
72+ , _FullRoute_Frontend
73+ , _FullRoute_Backend
74+ , mkFullRouteEncoder
6975 , ObeliskRoute (..)
7076 , _ObeliskRoute_App
7177 , _ObeliskRoute_Resource
@@ -90,7 +96,9 @@ module Obelisk.Route
9096 , Decoder(..)
9197 , dmapEncoder
9298 , fieldMapEncoder
99+ , pathFieldEncoder
93100 , jsonEncoder
101+ , byteStringsToPageName
94102 ) where
95103
96104import Prelude hiding ((.), id)
@@ -101,8 +109,31 @@ import qualified Control.Categorical.Functor as Cat
101109import Control.Categorical.Bifunctor
102110import Control.Category.Associative
103111import Control.Category.Monoidal
104−import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
112+import Control.Category.Braided
113+import Control.Lens
114+ ( Identity (..)
115+ , (^.)
116+ , (^?)
117+ , _Just
118+ , _Nothing
119+ , Cons(..)
120+ , from
121+ , imap
122+ , iso
123+ , Iso'
124+ , itraverse
125+ , makePrisms
126+ , Prism'
127+ , prism'
128+ , re
129+ , view
130+ , Wrapped (..)
131+ )
105132import Control.Monad.Except
133+import Control.Monad.Writer (execWriter, tell)
134+import qualified Control.Monad.State.Strict as State
135+import Control.Monad.Trans (lift)
136+import qualified Data.ByteString as BS
106137import qualified Data.ByteString.Lazy as BSL
107138import Data.Dependent.Sum (DSum (..))
108139import Data.Dependent.Map (DMap)
@@ -202,7 +233,7 @@ decode e x = runIdentity (tryDecode e x)
202233tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
203234tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
204235
205−-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
236+-- | Similar to 'decode', once an encoder has been checked so that its check monad is Identity, it
206237-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
207238-- one should usually be applying decode and encode to the same 'Encoder'
208239encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
@@ -252,6 +283,10 @@ instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (E
252283 , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
253284 }
254285
286+instance (Monad parse, Applicative check) => Braided (Encoder check parse) (,) where
287+ braid = isoEncoder (iso swap swap)
288+
289+
255290instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
256291 first f = bimap f id
257292instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
@@ -265,6 +300,34 @@ instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (Enco
265300 , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
266301 }
267302
303+instance Monad parse => PFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
304+ first f = bimap f id
305+instance Monad parse => QFunctor Either (EncoderImpl parse) (EncoderImpl parse) where
306+ second g = bimap id g
307+instance Monad parse => Bifunctor Either (EncoderImpl parse) (EncoderImpl parse) (EncoderImpl parse) where
308+ bimap f g = EncoderImpl
309+ { _encoderImpl_encode = bimap (_encoderImpl_encode f) (_encoderImpl_encode g)
310+ , _encoderImpl_decode = \case
311+ Left a -> Left <$> _encoderImpl_decode f a
312+ Right b -> Right <$> _encoderImpl_decode g b
313+ }
314+
315+instance (Monad parse, Applicative check) => QFunctor Either (Encoder check parse) (Encoder check parse) where
316+ second g = bimap id g
317+instance (Monad parse, Applicative check) => PFunctor Either (Encoder check parse) (Encoder check parse) where
318+ first f = bimap f id
319+instance (Monad parse, Applicative check) => Bifunctor Either (Encoder check parse) (Encoder check parse) (Encoder check parse) where
320+ bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
321+
322+instance (Applicative check, Monad parse) => Associative (Encoder check parse) Either where
323+ associate = isoEncoder (iso (associate @(->) @Either) disassociate)
324+ disassociate = isoEncoder (iso disassociate associate)
325+
326+instance (Monad parse, Applicative check) => Braided (Encoder check parse) Either where
327+ braid = isoEncoder (iso swap swap)
328+
329+
330+
268331instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
269332 fmap e = Encoder $ do
270333 ve <- unEncoder e
@@ -442,8 +505,9 @@ chainEncoder cons this rest = Encoder $ do
442505 pure $ EncoderImpl
443506 { _encoderImpl_decode = \v -> do
444507 (here, following) <- _encoderImpl_decode consValid v
445− Some r <- _encoderImpl_decode thisValid here
446− (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
508+ _encoderImpl_decode thisValid here >>= \case
509+ Some r ->
510+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
447511 , _encoderImpl_encode = \(r :/ s) ->
448512 _encoderImpl_encode consValid
449513 ( _encoderImpl_encode thisValid $ Some r
@@ -663,10 +727,47 @@ joinPairTextEncoder = Encoder . \case
663727 _ -> return (kt, T.drop (T.length separator) vt)
664728 }
665729
666−--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
667−-- Or, more likely, the user can compose it themselves
668−rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
669−rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
730+-- This slight generalization of 'rPrism' happens to be enough to write all our
731+-- prism combinators so far.
732+dSumPrism
733+ :: forall f f' g
734+ . (forall a. Prism' (f a) (f' a))
735+ -> Prism' (DSum f g) (DSum f' g)
736+dSumPrism p = prism'
737+ (\(f' :=> x) -> f' ^. re p :=> x)
738+ (\(f :=> x) -> (:=> x) <$> (f ^? p))
739+
740+-- already in obelisk
741+rPrism
742+ :: forall f f'
743+ . (forall a. Prism' (f a) (f' a))
744+ -> Prism' (R f) (R f')
745+rPrism p = dSumPrism p
746+
747+dSumPrism'
748+ :: forall f g a
749+ . (forall b. Prism' (f b) (a :~: b))
750+ -> Prism' (DSum f g) (g a)
751+dSumPrism' p = dSumPrism p . iso (\(Refl :=> b) -> b) (Refl :=>)
752+
753+dSumGEqPrism
754+ :: GEq f
755+ => f a
756+ -> Prism' (DSum f g) (g a)
757+dSumGEqPrism variant = dSumPrism' $ prism' (\Refl -> variant) (\x -> geq variant x)
758+
759+-- | Given a 'tag :: f a', make a prism for 'R f'. This generalizes the usual
760+-- prisms for a sum type (the ones that 'mkPrisms' would make), just as 'R'
761+-- generalized a usual sum type.
762+--
763+-- [This is given the '_R' name of the "cannonical" prism not because it is the
764+-- most general, but because it seems the most useful for routes, and 'R' itself
765+-- trades generality for route-specificity.]
766+_R
767+ :: GEq f
768+ => f a
769+ -> Prism' (R f) a
770+_R variant = dSumGEqPrism variant . iso runIdentity Identity
670771
671772-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
672773-- if the prism doesn't match.
@@ -710,6 +811,45 @@ handleEncoder recover e = Encoder $ do
710811-- Actual obelisk route info
711812--------------------------------------------------------------------------------
712813
814+-- | The typical full route type comprising all of an Obelisk application's routes.
815+-- Parameterised by the top level GADTs that define backend and frontend routes, respectively.
816+data FullRoute :: (* -> *) -> (* -> *) -> * -> * where
817+ FullRoute_Backend :: br a -> FullRoute br fr a
818+ FullRoute_Frontend :: ObeliskRoute fr a -> FullRoute br fr a
819+
820+instance (GShow br, GShow fr) => GShow (FullRoute br fr) where
821+ gshowsPrec p = \case
822+ FullRoute_Backend x -> showParen (p > 10) (showString "FullRoute_Backend " . gshowsPrec 11 x)
823+ FullRoute_Frontend x -> showParen (p > 10) (showString "FullRoute_Frontend " . gshowsPrec 11 x)
824+
825+instance (GEq br, GEq fr) => GEq (FullRoute br fr) where
826+ geq (FullRoute_Backend x) (FullRoute_Backend y) = geq x y
827+ geq (FullRoute_Frontend x) (FullRoute_Frontend y) = geq x y
828+ geq _ _ = Nothing
829+
830+instance (GCompare br, GCompare fr) => GCompare (FullRoute br fr) where
831+ gcompare (FullRoute_Backend _) (FullRoute_Frontend _) = GLT
832+ gcompare (FullRoute_Frontend _) (FullRoute_Backend _) = GGT
833+ gcompare (FullRoute_Backend x) (FullRoute_Backend y) = gcompare x y
834+ gcompare (FullRoute_Frontend x) (FullRoute_Frontend y) = gcompare x y
835+
836+instance (UniverseSome br, UniverseSome fr) => UniverseSome (FullRoute br fr) where
837+ universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
838+ ++ [Some (FullRoute_Frontend x) | Some x <- universeSome]
839+
840+-- | Build the typical top level application route encoder from a route for handling 404's,
841+-- and segment encoders for backend and frontend routes.
842+mkFullRouteEncoder
843+ :: (GCompare br, GCompare fr, GShow br, GShow fr, UniverseSome br, UniverseSome fr)
844+ => R (FullRoute br fr) -- ^ 404 handler
845+ -> (forall a. br a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single backend route segment
846+ -> (forall a. fr a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single frontend route segment
847+ -> Encoder (Either Text) Identity (R (FullRoute br fr)) PageName
848+mkFullRouteEncoder missing backendSegment frontendSegment = handleEncoder (const missing) $
849+ pathComponentEncoder $ \case
850+ FullRoute_Backend backendRoute -> backendSegment backendRoute
851+ FullRoute_Frontend obeliskRoute -> obeliskRouteSegment obeliskRoute frontendSegment
852+
713853-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
714854-- frontend.
715855data ObeliskRoute :: (* -> *) -> * -> * where
@@ -839,7 +979,7 @@ concat <$> mapM deriveRouteComponent
839979 ]
840980
841981makePrisms ''ObeliskRoute
842−
982+makePrisms ''FullRoute
843983deriveGEq ''Void1
844984deriveGCompare ''Void1
845985
@@ -847,27 +987,27 @@ deriveGCompare ''Void1
847987-- and query string). See 'checkEncoder' for how to produce a checked encoder.
848988renderBackendRoute
849989 :: forall br a.
850− Encoder Identity Identity (R (Sum br a)) PageName
990+ Encoder Identity Identity (R (FullRoute br a)) PageName
851991 -> R br
852992 -> Text
853−renderBackendRoute enc = renderObeliskRoute enc . hoistR InL
993+renderBackendRoute enc = renderObeliskRoute enc . hoistR FullRoute_Backend
854994
855995-- | Renders a frontend route with the supplied checked encoder
856996renderFrontendRoute
857997 :: forall a fr.
858− Encoder Identity Identity (R (Sum a (ObeliskRoute fr))) PageName
998+ Encoder Identity Identity (R (FullRoute a fr)) PageName
859999 -> R fr
8601000 -> Text
861−renderFrontendRoute enc = renderObeliskRoute enc . hoistR (InR . ObeliskRoute_App)
1001+renderFrontendRoute enc = renderObeliskRoute enc . hoistR (FullRoute_Frontend . ObeliskRoute_App)
8621002
8631003-- | Renders a route of the form typically found in an Obelisk project
8641004renderObeliskRoute
8651005 :: forall a b.
866− Encoder Identity Identity (R (Sum a b)) PageName
867− -> R (Sum a b)
1006+ Encoder Identity Identity (R (FullRoute a b)) PageName
1007+ -> R (FullRoute a b)
8681008 -> Text
8691009renderObeliskRoute e r =
870− let enc :: Encoder Identity (Either Text) (R (Sum a b)) PathQuery
1010+ let enc :: Encoder Identity (Either Text) (R (FullRoute a b)) PathQuery
8711011 enc = (pageNameEncoder . hoistParse (pure . runIdentity) e)
8721012 in (T.pack . uncurry (<>)) $ encode enc r
8731013
@@ -877,14 +1017,9 @@ readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
8771017integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
8781018integralEncoder = prismEncoder (Numeric.Lens.integral)
8791019
880−pathSegmentEncoder :: (MonadError Text parse, Applicative check) =>
881− Encoder check parse (Text, PageName) PageName
882−pathSegmentEncoder = unsafeMkEncoder EncoderImpl
883− { _encoderImpl_encode = \(x, (y, z)) -> (x:y, z)
884− , _encoderImpl_decode = \(xss, y) -> case xss of
885− [] -> throwError "not enough path segments"
886− (x:xs) -> pure (x, (xs, y))
887− }
1020+pathSegmentEncoder :: (MonadError Text parse, Applicative check, Cons as as a a) =>
1021+ Encoder check parse (a, (as, b)) (as, b)
1022+pathSegmentEncoder = first (prismEncoder _Cons) . disassociate
8881023
8891024newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
8901025
@@ -912,12 +1047,12 @@ dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
9121047 , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
9131048 )
9141049 , _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')
1050+ tryDecode keyEncoder k >>= \case
1051+ Some (k' :: k' t) -> case DMap.lookup k' valueDecoders of
1052+ Nothing -> throwError . T.pack . keyError $ gshow k'
1053+ Just (Decoder e) -> do
1054+ v' <- tryDecode e v
1055+ return (k' :=> Identity v')
9211056 }
9221057
9231058fieldMapEncoder :: forall check parse r.
@@ -938,6 +1073,32 @@ fieldMapEncoder = unsafeEncoder $ do
9381073 Just (Identity v) -> return v
9391074 }
9401075
1076+-- this is in base 4.12 (GHC 8.6);
1077+newtype Ap f a = Ap {getAp :: f a}
1078+
1079+instance (Applicative f, Semigroup a) => Semigroup (Ap f a) where
1080+ Ap x <> Ap y = Ap (liftA2 (<>) x y)
1081+
1082+instance (Applicative f, Monoid a) => Monoid (Ap f a) where
1083+ mappend = (<>)
1084+ mempty = Ap (pure mempty)
1085+
1086+pathFieldEncoder :: forall a p check parse . (HasFields a, Monad check, MonadError Text parse, GCompare (Field a)) => (forall x. Field a x -> Encoder check parse x p) -> Encoder check parse (a, [p]) [p]
1087+pathFieldEncoder fieldEncoder = unsafeEncoder $ do
1088+ fieldEncoderPureMap :: DMap.DMap (Field a) (Decoder Identity parse p) <- getAp $ getConst $ tabulateFieldsA @a $ \f -> Const (Ap $ fmap (DMap.singleton f . Decoder) $ (checkEncoder @Identity) $ fieldEncoder f)
1089+ let fieldEncoderPure :: forall x. Field a x -> Encoder Identity parse x p
1090+ fieldEncoderPure f = toEncoder (DMap.findWithDefault (error "bad") f fieldEncoderPureMap)
1091+ pure $ EncoderImpl
1092+ { _encoderImpl_encode = \(x, rest) -> execWriter $ do
1093+ _ <- traverseWithField (\f x_i -> tell (pure $ encode (fieldEncoderPure f) x_i) *> pure x_i) x
1094+ tell rest
1095+ , _encoderImpl_decode = State.runStateT $ tabulateFieldsA $ \f -> State.get >>= \case
1096+ [] -> throwError $ T.pack "not enough path components"
1097+ p:ps -> do
1098+ State.put ps
1099+ lift $ tryDecode (fieldEncoderPure f) p
1100+ }
1101+
9411102-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
9421103jsonEncoder :: forall check parse r.
9431104 ( ToJSON r
@@ -954,4 +1115,14 @@ jsonEncoder = unsafeEncoder $ do
9541115 Right x -> return x
9551116 }
9561117
1118+-- Useful for app server integration.
1119+-- p must not start with slashes
1120+byteStringsToPageName :: BS.ByteString -> BS.ByteString -> PageName
1121+byteStringsToPageName p q =
1122+ let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
1123+ pageNameEncoder' = bimap
1124+ (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
1125+ (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
1126+ in decode pageNameEncoder' (T.unpack (T.decodeUtf8 p), T.unpack (T.decodeUtf8 q))
1127+
9571128--TODO: decodeURIComponent as appropriate
lib/run/src/Obelisk/Run.hs+11−12
@@ -20,11 +20,10 @@ import Control.Exception
2020import Control.Lens ((%~), (^?), _Just, _Right)
2121import qualified Data.Attoparsec.ByteString.Char8 as A
2222import Data.ByteString (ByteString)
23+import qualified Data.ByteString as BS
2324import qualified Data.ByteString.Char8 as BSC
2425import qualified Data.ByteString.Lazy.Char8 as BSLC
25−import Data.Dependent.Sum (DSum (..))
2626import Data.Functor.Identity
27−import Data.Functor.Sum
2827import Data.List (uncons)
2928import Data.Map (Map)
3029import qualified Data.Map as Map
@@ -81,8 +80,8 @@ run port serveStaticAsset backend frontend = do
8180 runSnapWithCommandLineArgs $ do
8281 getRouteWith validFullEncoder >>= \case
8382 Identity r -> case r of
84− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
85− InR obeliskRoute :=> Identity a ->
83+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
84+ FullRoute_Frontend obeliskRoute :/ a ->
8685 serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
8786 let conf = defRunConfig { _runConfig_redirectPort = port }
8887 runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
@@ -103,8 +102,8 @@ getConfigRoute configs = case Map.lookup "common/route" configs of
103102runWidget
104103 :: RunConfig
105104 -> Map Text ByteString
106− -> Frontend (R route)
107− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
105+ -> Frontend (R frontendRoute)
106+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
108107 -> IO ()
109108runWidget conf configs frontend validFullEncoder = do
110109 uri <- either (fail . T.unpack) pure $ getConfigRoute configs
@@ -123,11 +122,11 @@ runWidget conf configs frontend validFullEncoder = do
123122 runSettingsSocket settings skt app)
124123
125124obeliskApp
126− :: forall route backendRoute
125+ :: forall frontendRoute backendRoute
127126 . Map Text ByteString
128127 -> ConnectionOptions
129− -> Frontend (R route)
130− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
128+ -> Frontend (R frontendRoute)
129+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
131130 -> URI
132131 -> Application
133132 -> IO Application
@@ -143,14 +142,14 @@ obeliskApp configs opts frontend validFullEncoder uri backend = do
143142 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
144143 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
145144 jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
146− return $ \req sendResponse -> case tryDecode validFullEncoder (W.pathInfo req, mempty) of --TODO: Query strings
145+ return $ \req sendResponse -> case tryDecode validFullEncoder $ byteStringsToPageName (BS.dropWhile (== (fromIntegral $ fromEnum '/')) $ W.rawPathInfo req) (BS.drop 1 $ W.rawQueryString req) of
147146 Identity r -> case r of
148− InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
147+ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :/ jsaddleRoute -> case jsaddleRoute of
149148 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
150149 _ -> flip jsaddle sendResponse $ req
151150 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
152151 }
153− InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
152+ FullRoute_Frontend (ObeliskRoute_App appRouteComponent) :/ appRouteRest -> do
154153 let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
155154 html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
156155 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
lib/selftest/src/Obelisk/SelfTest.hs+12−15
@@ -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
@@ -128,8 +129,8 @@ main = do
128129 ls tmp >>= liftIO . assertEqual "" []
129130
130131 it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
131− configs <- getConfigs
132− withCurrentDirectory (T.unpack $ toTextIgnore tmp) $
132+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
133+ configs <- getConfigs
133134 return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
134135
135136 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
@@ -141,33 +142,29 @@ main = do
141142 testObRunInDir p2 p3 (Just "frontend") httpManager
142143
143144 describe "obelisk project" $ parallel $ do
144− it "can build obelisk command" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "command" , obeliskImpl]
145− it "can build obelisk skeleton" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "skeleton", obeliskImpl]
146− it "can build obelisk shell" $ inTmpObInit $ \_ -> run "nix-build" ["-A", "shell", obeliskImpl]
147− -- See https://github.com/obsidiansystems/obelisk/issues/101
148− -- it "can build everything" $ shelly_ $ run "nix-build" [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]
148+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [obeliskImpl]
149149
150150 describe "blank initialized project" $ parallel $ do
151151
152152 it "can build ghc.backend" $ inTmpObInit $ \_ -> do
153− run "nix-build" ["--no-out-link", "-A", "ghc.backend"]
153+ nixBuild ["-A", "ghc.backend"]
154154 it "can build ghcjs.frontend" $ inTmpObInit $ \_ -> do
155− run "nix-build" ["--no-out-link", "-A", "ghcjs.frontend"]
155+ nixBuild ["-A", "ghcjs.frontend"]
156156
157157 if os == "darwin"
158− then it "can build ios" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "ios.frontend" ]
159− else it "can build android" $ inTmpObInit $ \_ -> run "nix-build" ["--no-out-link", "-A", "android.frontend"]
158+ then it "can build ios" $ inTmpObInit $ \_ -> nixBuild ["-A", "ios.frontend"]
159+ else it "can build android" $ inTmpObInit $ \_ -> nixBuild ["-A", "android.frontend"]
160160
161161 forM_ ["ghc", "ghcjs"] $ \compiler -> do
162162 let
163163 shell = "shells." <> compiler
164− inShell cmd' = run "nix-shell" ["-A", fromString shell, "--run", cmd']
164+ inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shell, "--run", cmd']
165165 it ("can enter " <> shell) $ inTmpObInit $ \_ -> inShell "exit"
166166 it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
167167
168− it "can build reflex project" $ inTmpObInit $ \_ -> do
169− run "nix-build" []
170−
171168 it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
172169 u <- update
173170 uu <- update
release.nix+3−6
@@ -58,6 +58,7 @@ let
5858 (builtins.attrValues ghcjs)
5959 (concatDepends ghc)
6060 (concatDepends ghcjs)
61+ [command serverExeSkeleton iosSkeleton androidSkeleton]
6162 ];
6263 command = local-self.command;
6364 serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
@@ -77,15 +78,11 @@ let
7778 serverExeSkeleton
7879 iosSkeleton
7980 androidSkeleton;
80− cache = reflex-platform.pinBuildInputs
81− "obelisk-${system}"
82− cachePackages
83− [command serverExeSkeleton iosSkeleton androidSkeleton];
81+ cache = reflex-platform.pinBuildInputs "obelisk-${system}" cachePackages;
8482 });
8583
8684 metaCache = local-self.pinBuildInputs
8785 "obelisk-everywhere"
88− (map (a: a.cache) (builtins.attrValues perPlatform))
89− [];
86+ (map (a: a.cache) (builtins.attrValues perPlatform));
9087
9188in perPlatform // { inherit metaCache; }
skeleton/backend/src/Backend.hs+1−3
@@ -1,6 +1,4 @@
1−{-# LANGUAGE EmptyCase #-}
21{-# LANGUAGE FlexibleContexts #-}
3−{-# LANGUAGE LambdaCase #-}
42{-# LANGUAGE RankNTypes #-}
53{-# LANGUAGE TypeFamilies #-}
64module Backend where
@@ -11,5 +9,5 @@ import Obelisk.Backend
119backend :: Backend BackendRoute FrontendRoute
1210backend = Backend
1311 { _backend_run = \serve -> serve $ const $ return ()
14− , _backend_routeEncoder = backendRouteEncoder
12+ , _backend_routeEncoder = fullRouteEncoder
1513 }
skeleton/common/src/Common/Route.hs+12−18
@@ -1,15 +1,13 @@
11{-# LANGUAGE EmptyCase #-}
22{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
34{-# LANGUAGE GADTs #-}
4−{-# LANGUAGE LambdaCase #-}
5−{-# LANGUAGE RankNTypes #-}
6−{-# LANGUAGE TemplateHaskell #-}
75{-# LANGUAGE KindSignatures #-}
8−{-# LANGUAGE EmptyCase #-}
6+{-# LANGUAGE LambdaCase #-}
97{-# LANGUAGE MultiParamTypeClasses #-}
10−{-# LANGUAGE FlexibleInstances #-}
118{-# LANGUAGE OverloadedStrings #-}
12−
9+{-# LANGUAGE RankNTypes #-}
10+{-# LANGUAGE TemplateHaskell #-}
1311module Common.Route where
1412
1513{- -- You will probably want these imports for composing Encoders.
@@ -19,7 +17,6 @@ import Control.Category
1917
2018import Data.Text (Text)
2119import Data.Functor.Identity
22−import Data.Functor.Sum
2320
2421import Obelisk.Route
2522import Obelisk.Route.TH
@@ -34,17 +31,14 @@ data FrontendRoute :: * -> * where
3431 FrontendRoute_Main :: FrontendRoute ()
3532 -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
3633
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
47−
34+fullRouteEncoder
35+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
36+fullRouteEncoder = mkFullRouteEncoder
37+ (FullRoute_Backend BackendRoute_Missing :/ ())
38+ (\case
39+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
40+ (\case
41+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
4842
4943concat <$> mapM deriveRouteComponent
5044 [ ''BackendRoute
skeleton/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 Right validFullEncoder = checkEncoder fullRouteEncoder
1010 run $ runFrontend validFullEncoder frontend
skeleton/frontend/src/Frontend.hs+1−1
@@ -28,7 +28,7 @@ frontend = Frontend
2828 el "p" $ text $ T.pack commonStuff
2929 elAttr "img" ("src" =: static @"obelisk.jpg") blank
3030 el "div" $ do
31− exampleConfig <- getConfig "common/example"
31+ exampleConfig <- getConfig "common/example"
3232 case exampleConfig of
3333 Nothing -> text "No config file found in config/common/example"
3434 Just s -> text (T.decodeUtf8 s)
skeleton/shell.nixnew file+1
@@ -0,0 +1 @@
1+(import ./. {}).shells.ghc