on master help sign in
Merge remote-tracking branch 'origin/develop' into aa-use-ghc-options

* origin/develop: (30 commits) Bump reflex-platform Fix release.nix - pinBuildInputs has changed in r-p Bump reflex-platform to fix missing ghcjs maybe tycon Update changelog Update for new hnix Update for new hpack Updates for new git library and megaparsec Bump reflex-platform; More missing MonadFail Add MonadFail constraints in obelisk-cliapp Fix hnix Bump hnix; Updates for MonadFail obelisk-route: Fix "incomplete" pattern match Bump reflex-platform (now uses ghc 8.6.5) Add changelog entry for ob doc Add to changelog Use FullRoute in types of render{Backend,Frontend,Obelisk}Route Add a getTextConfig convenience function. Add a FullRoute type to formalise the expected top level structure of obelisk routes. Add an ob doc command to get links to documentation. bump reflex-platform, fix warnings, remove a chunk of commented-out code. ...

commit486ef09fcafd047f4bae609340f89e908778a7f7 authorAli Abrar <aliabrar@gmail.com> dateTue 1 Oct 2019, 16:36 (7 years ago) parent8319599f02b2c3
24 files changed+354−147
ChangeLog.md+5 default.nix+14−10 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+12−16 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+16−16 lib/command/src/Obelisk/Command/Run.hs+7−6 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 release.nix+3−6 skeleton/backend/src/Backend.hs+1−1 skeleton/common/src/Common/Route.hs+8−12 skeleton/default.nix+6−6 skeleton/frontend/src-bin/main.hs+1−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.
default.nix+14−10
@@ -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
@@ -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 = {
@@ -269,7 +273,7 @@ in rec {
269273 , tools ? _: []
270274 , shellToolOverrides ? _: _: {}
271275 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
272− , __closureCompilerOptimizationLevel ? "ADVANCED"
276+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
273277 }:
274278 let frontendName = "frontend";
275279 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": "83dda45e2a91e6c657fe82bc63415e6dc283dc1a",
6+ "sha256": "1j7mhs1pn1c81zzx67fja1iirc683nw9ryi63p0wspr2cv5hs5p9"
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+12−16
@@ -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
@@ -52,7 +52,7 @@ import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog
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
@@ -174,7 +170,7 @@ serveGhcjsApp urlEnc app config = \case
174170 writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
175171 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
176172
177−runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
173+runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
178174runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
179175 Left e -> fail $ "backend error:\n" <> T.unpack e
180176 Right validFullEncoder -> do
@@ -183,15 +179,15 @@ runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend
183179 runSnapWithCommandLineArgs $ do
184180 getRouteWith validFullEncoder >>= \case
185181 Identity r -> case r of
186− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
187− InR obeliskRoute :=> Identity a ->
182+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
183+ FullRoute_Frontend obeliskRoute :/ a ->
188184 serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
189185 obeliskRoute :/ a
190186
191−mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
187+mkRouteToUrl :: Encoder Identity parse (R (FullRoute br fr)) PageName -> R fr -> Text
192188mkRouteToUrl validFullEncoder =
193189 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
190+ in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (FullRoute_Frontend $ ObeliskRoute_App k) :/ v
195191
196192renderGhcjsFrontend
197193 :: (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+16−16
@@ -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
lib/command/src/Obelisk/Command/Run.hs+7−6
@@ -121,12 +121,12 @@ parseCabalPackage dir = do
121121 then Just <$> liftIO (readUTF8File cabalFp)
122122 else if hasHpack
123123 then do
124− let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
124+ let decodeOptions = DecodeOptions (ProgramName "ob") hpackFp Nothing decodeYaml
125125 liftIO (readPackageConfig decodeOptions) >>= \case
126126 Left err -> do
127127 putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
128128 return Nothing
129− Right (DecodeResult hpackPackage _ _) -> do
129+ Right (DecodeResult hpackPackage _ _ _) -> do
130130 return $ Just $ renderPackage [] hpackPackage
131131 else return Nothing
132132
@@ -182,12 +182,13 @@ withGhciScript pkgs f = do
182182 extensionsLine = if extensions == mempty
183183 then ""
184184 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
185− ghcOptions = (":set " <>) $ intercalate " " $ concat $
186− mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
187− packageInfos >>= _cabalPackageInfo_compilerOptions
185+ ghcOptions = concat $ mapMaybe (\case (GHC, xs) -> Just xs; _ -> Nothing) $
186+ packageInfos >>= _cabalPackageInfo_compilerOptions
188187 dotGhci = unlines $
189188 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
190− , ghcOptions
189+ , case ghcOptions of
190+ [] -> ""
191+ xs -> ":set " <> intercalate " " xs
191192 , extensionsLine
192193 , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
193194 , ":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
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−1
@@ -11,5 +11,5 @@ import Obelisk.Backend
1111backend :: Backend BackendRoute FrontendRoute
1212backend = Backend
1313 { _backend_run = \serve -> serve $ const $ return ()
14− , _backend_routeEncoder = backendRouteEncoder
14+ , _backend_routeEncoder = fullRouteEncoder
1515 }
skeleton/common/src/Common/Route.hs+8−12
@@ -19,7 +19,6 @@ import Control.Category
1919
2020import Data.Text (Text)
2121import Data.Functor.Identity
22−import Data.Functor.Sum
2322
2423import Obelisk.Route
2524import Obelisk.Route.TH
@@ -34,17 +33,14 @@ data FrontendRoute :: * -> * where
3433 FrontendRoute_Main :: FrontendRoute ()
3534 -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
3635
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−
36+fullRouteEncoder
37+ :: Encoder (Either Text) Identity (R (FullRoute BackendRoute FrontendRoute)) PageName
38+fullRouteEncoder = mkFullRouteEncoder
39+ (FullRoute_Backend BackendRoute_Missing :/ ())
40+ (\case
41+ BackendRoute_Missing -> PathSegment "missing" $ unitEncoder mempty)
42+ (\case
43+ FrontendRoute_Main -> PathEnd $ unitEncoder mempty)
4844
4945concat <$> mapM deriveRouteComponent
5046 [ ''BackendRoute
skeleton/default.nix+6−6
@@ -1,10 +1,10 @@
11{ obelisk ? import ./.obelisk/impl {
2− system = builtins.currentSystem;
3− iosSdkVersion = "10.2";
4− # You must accept the Android Software Development Kit License Agreement at
5− # https://developer.android.com/studio/terms in order to build Android apps.
6− # Uncomment and set this to `true` to indicate your acceptance:
7− # config.android_sdk.accept_license = false;
2+ system = builtins.currentSystem;
3+ iosSdkVersion = "10.2";
4+ # You must accept the Android Software Development Kit License Agreement at
5+ # https://developer.android.com/studio/terms in order to build Android apps.
6+ # Uncomment and set this to `true` to indicate your acceptance:
7+ # config.android_sdk.accept_license = false;
88 }
99}:
1010with obelisk;
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