on master help sign in
Merge remote-tracking branch 'origin/develop' into upstream-optparse
commitf40e5875c360db88ebcc42a0eec7e3e574724a04 authorJohn Ericson <John.Ericson@Obsidian.Systems> dateMon 7 Oct 2019, 18:20 (7 years ago) parent2105803b85c9ee
28 files changed+407−166
ChangeLog.md+5 README.md+40−9 default.nix+15−11 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+11−2 lib/command/src/Obelisk/Command/Thunk.hs+2−2 lib/command/src/Obelisk/Command/Utils.hs+1−1 lib/executable-config/lookup/src/Obelisk/Configs.hs+4 lib/frontend/src/Obelisk/Frontend.hs+6−5 lib/route/src/Obelisk/Route.hs+200−29 lib/run/src/Obelisk/Run.hs+11−12 lib/selftest/src/Obelisk/SelfTest.hs+4−8 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
ChangeLog.md+5
@@ -4,6 +4,11 @@ This project's release branch is `master`. This log is written from the perspect
44
55## Unreleased
66
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+
712## v0.2.0.0 - 2019-8-17
813
914* Configs become ByteStrings.
README.md+40−9
@@ -60,6 +60,28 @@ Or to launch ghcid for `lib/command` project:
6060nix-shell -A obeliskEnvs.obelisk-command --run "cd lib/command && ghcid -c 'cabal new-repl'"
6161```
6262
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+
6385### Accessing private repositories
6486
6587To allow the Nix builder to access private git repositories, you must be set up
@@ -99,14 +121,14 @@ In order to add package dependencies, declare them under the build-depends field
99121
100122### Adding package overrides
101123
102−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:
103125
104126```nix
105127# ...
106128project ./. ({ pkgs, ... }: {
107129# ...
108130 overrides = self: super: let
109− aeson = pkgs.fetchFromGitHub {
131+ aesonSrc = pkgs.fetchFromGitHub {
110132 owner = "obsidiansystems";
111133 repo = "aeson-gadt-th";
112134 rev = "ed573c2cccf54d72aa6279026752a3fecf9c1383";
@@ -114,7 +136,12 @@ project ./. ({ pkgs, ... }: {
114136 };
115137 in
116138 {
117− 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+ } {};
118145 };
119146# ...
120147```
@@ -128,14 +155,16 @@ For further information see [the Haskell section](https://nixos.org/nixpkgs/manu
128155Build everything:
129156
130157```
131−nix-build -A exe -o result-exe
158+nix-build -A exe --no-out-link
132159```
133160
134−Run the server:
161+Copy the result to a new directory, add configuration, and run!
135162
136163```
137−cd result-exe
138−./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)
139168```
140169
141170### EC2
@@ -167,9 +196,11 @@ ob deploy init \
167196 ~/code/myapp-deploy
168197```
169198
170−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.
171202
172−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!
173204
174205```
175206cd ~/code/myapp-deploy
default.nix+15−11
@@ -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
@@ -108,7 +108,7 @@ in rec {
108108 obelisk = ghcObelisk;
109109 obeliskEnvs = ghcObeliskEnvs;
110110 command = ghcObelisk.obelisk-command;
111− shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
111+ shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs);
112112
113113 selftest = pkgs.writeScript "selftest" ''
114114 #!/usr/bin/env bash
@@ -137,12 +137,16 @@ in rec {
137137 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
138138 '';
139139
140− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
140+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
141141 mkdir $out
142142 cd $out
143143 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
144− 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
145− echo "//# sourceMappingURL=all.js.map" >> all.js
144+ ${if optimizationLevel == null then ''
145+ ln -s all.unminified.js all.js
146+ '' else ''
147+ ${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
148+ echo "//# sourceMappingURL=all.js.map" >> all.js
149+ ''}
146150 '';
147151
148152 serverModules = {
@@ -249,7 +253,7 @@ in rec {
249253 , tools ? _: []
250254 , shellToolOverrides ? _: _: {}
251255 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
252− , __closureCompilerOptimizationLevel ? "ADVANCED"
256+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
253257 }:
254258 let frontendName = "frontend";
255259 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+11−2
@@ -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.
@@ -118,12 +121,12 @@ parseCabalPackage dir = do
118121 then Just <$> liftIO (readUTF8File cabalFp)
119122 else if hasHpack
120123 then do
121− let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
124+ let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
122125 liftIO (readPackageConfig decodeOptions) >>= \case
123126 Left err -> do
124127 putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
125128 return Nothing
126− Right (DecodeResult hpackPackage _ _) -> do
129+ Right (DecodeResult hpackPackage _ _ _) -> do
127130 return $ Just $ renderPackage [] hpackPackage
128131 else return Nothing
129132
@@ -143,6 +146,7 @@ parseCabalPackage dir = do
143146 defaultExtensions $ libBuildInfo lib
144147 , _cabalPackageInfo_defaultLanguage =
145148 defaultLanguage $ libBuildInfo lib
149+ , _cabalPackageInfo_compilerOptions = options $ libBuildInfo lib
146150 }
147151 Left (_, errors) -> do
148152 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -178,8 +182,13 @@ withGhciScript pkgs f = do
178182 extensionsLine = if extensions == mempty
179183 then ""
180184 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
185+ ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
186+ packageInfos >>= _cabalPackageInfo_compilerOptions
181187 dotGhci = unlines $
182188 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
189+ , case ghcOptions of
190+ [] -> ""
191+ xs -> ":set " <> intercalate " " xs
183192 , extensionsLine
184193 , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
185194 , ":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/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+6−5
@@ -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 =
@@ -144,7 +145,7 @@ data FrontendMode = FrontendMode
144145-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
145146runFrontend
146147 :: forall backendRoute route
147− . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
148+ . Encoder Identity Identity (R (FullRoute backendRoute route)) PageName
148149 -> Frontend (R route)
149150 -> JSM ()
150151runFrontend validFullEncoder frontend = do
@@ -172,14 +173,14 @@ runFrontend validFullEncoder frontend = do
172173 runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
173174
174175runFrontendWithConfigsAndCurrentRoute
175− :: forall backendRoute route
176+ :: forall backendRoute frontendRoute
176177 . FrontendMode
177178 -> Map Text ByteString
178− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
179− -> Frontend (R route)
179+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
180+ -> Frontend (R frontendRoute)
180181 -> JSM ()
181182runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
182− let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
183+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
183184 errorLeft = \case
184185 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
185186 Right x -> Identity x
lib/route/src/Obelisk/Route.hs+200−29
@@ -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)
@@ -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+4−8
@@ -129,8 +129,8 @@ main = do
129129 ls tmp >>= liftIO . assertEqual "" []
130130
131131 it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
132− configs <- getConfigs
133− withCurrentDirectory (T.unpack $ toTextIgnore tmp) $
132+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ do
133+ configs <- getConfigs
134134 return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
135135
136136 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
@@ -145,8 +145,7 @@ main = do
145145 it "can build obelisk command" $ inTmpObInit $ \_ -> nixBuild ["-A", "command" , obeliskImpl]
146146 it "can build obelisk skeleton" $ inTmpObInit $ \_ -> nixBuild ["-A", "skeleton", obeliskImpl]
147147 it "can build obelisk shell" $ inTmpObInit $ \_ -> nixBuild ["-A", "shell", obeliskImpl]
148− -- See https://github.com/obsidiansystems/obelisk/issues/101
149− -- it "can build everything" $ shelly_ $ nixBuild [obeliskImpl]
148+ it "can build everything" $ inTmpObInit $ \_ -> nixBuild [obeliskImpl]
150149
151150 describe "blank initialized project" $ parallel $ do
152151
@@ -162,13 +161,10 @@ main = do
162161 forM_ ["ghc", "ghcjs"] $ \compiler -> do
163162 let
164163 shell = "shells." <> compiler
165− inShell cmd' = run "nix-shell" ["-A", fromString shell, "--run", cmd']
164+ inShell cmd' = run "nix-shell" ["default.nix", "-A", fromString shell, "--run", cmd']
166165 it ("can enter " <> shell) $ inTmpObInit $ \_ -> inShell "exit"
167166 it ("can build in " <> shell) $ inTmpObInit $ \_ -> inShell $ "cabal new-build --" <> fromString compiler <> " all"
168167
169− it "can build reflex project" $ inTmpObInit $ \_ -> do
170− nixBuild []
171−
172168 it "has idempotent thunk update" $ inTmpObInit $ \_ -> do
173169 u <- update
174170 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
@@ -22,7 +22,7 @@ frontend = Frontend
2222 el "p" $ text $ T.pack commonStuff
2323 elAttr "img" ("src" =: static @"obelisk.jpg") blank
2424 el "div" $ do
25− exampleConfig <- getConfig "common/example"
25+ exampleConfig <- getConfig "common/example"
2626 case exampleConfig of
2727 Nothing -> text "No config file found in config/common/example"
2828 Just s -> text (T.decodeUtf8 s)
skeleton/shell.nixnew file+1
@@ -0,0 +1 @@
1+(import ./. {}).shells.ghc