on master help sign in
Merge commit '8c802b1' into kiln
commit4fd646c70a751ee3d894bdf54e965ffca982a162 authorDivam <dfordivam@gmail.com> dateSat 5 Oct 2019, 13:23 (7 years ago) parent87abcaa8c802b1
58 files changed+1339−412
.gitignore+1−3 CONTRIBUTING.md+11 ChangeLog.md+23 FAQ.md+76 README.md+3−1 default.nix+20−21 dep/reflex-platform/github.json+3−3 jobsets.nix+5 lib/asset/manifest/obelisk-asset-manifest.cabal+1 lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2 lib/backend/obelisk-backend.cabal+3 lib/backend/src/Obelisk/Backend.hs+42−31 lib/command/src/Obelisk/Command.hs+50−5 lib/command/src/Obelisk/Command/Deploy.hs+3−3 lib/command/src/Obelisk/Command/Project.hs+6−5 lib/command/src/Obelisk/Command/Run.hs+15−2 lib/command/src/Obelisk/Command/Thunk.hs+7−12 lib/executable-config/default.nix+4−1 lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2 lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40 lib/executable-config/lookup/default.nix+5−3 lib/executable-config/lookup/obelisk-executable-config-lookup.cabal+45 lib/executable-config/lookup/obelisk-executable-config.cabal−30 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hs−27 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hs+60 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hs−17 lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hs+42 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hs−22 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hs+17 lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hs+18 lib/executable-config/lookup/src/Obelisk/Configs.hs+128 lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hs+24 lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hs−13 lib/frontend/obelisk-frontend.cabal+2 lib/frontend/src/Obelisk/Frontend.hs+150−25 lib/frontend/src/Obelisk/Frontend/Cookie.hs+3 lib/route/obelisk-route.cabal+7−2 lib/route/src/Obelisk/Route.hs+298−53 lib/route/src/Obelisk/Route/Frontend.hs+51−20 lib/route/src/Obelisk/Route/TH.hs+4−5 lib/run/obelisk-run.cabal+2−1 lib/run/src/Obelisk/Run.hs+52−36 lib/selftest/obelisk-selftest.cabal+1 lib/selftest/src/Obelisk/SelfTest.hs+5−2 lib/tabulation/CHANGELOG.md+5 lib/tabulation/LICENSE+30 lib/tabulation/Setup.hs+2 lib/tabulation/src/Data/Tabulation.hs+21 lib/tabulation/tabulation.cabal+26 release.nix+17−6 skeleton/backend/src/Backend.hs+1−1 skeleton/common/src/Common/Api.hs+1−1 skeleton/common/src/Common/Route.hs+8−12 skeleton/config/common/example+1 skeleton/default.nix+9−3 skeleton/frontend/frontend.cabal+1 skeleton/frontend/src-bin/main.hs+1−1 skeleton/frontend/src/Frontend.hs+8−1
.gitignore+1−3
@@ -4,12 +4,10 @@
44result
55*.o
66*.hi
7−
87TAGS
98tags
109ctags
1110dist-newstyle
1211cabal.project.local
1312.ghc.environment.*
14−
15−**/ghcid-output.txt
13+**/ghcid-output.txt
CONTRIBUTING.md+11
@@ -28,6 +28,9 @@ The summary line of your commit message should summarize the changes being made.
2828One way to think about it is that your commit message should be able to complete the sentence:
2929"When applied, this commit will..."
3030
31+##### Note on bumping dependencies
32+Commits that update a dependency should include some information about why the dependency was updated in the commit message.
33+
3134#### Body
3235For breaking changes, new features, refactors, or other major changes, the body of the commit message should describe the motivation behind the change in greater detail and may include references to the issue tracker. The body shouldn't repeat code/comments from the diff.
3336
@@ -46,6 +49,14 @@ You can run the tests like this:
4649$(nix-build -A selftest --no-out-link)
4750```
4851
52+To test that your changes build across platforms, you can also try to build release.nix, like this:
53+```bash
54+nix-build release.nix
55+```
56+
57+Note, however, that to build release.nix you must accept the android license agreement and your machine must be configured to build both ios and android executables (usually via remote builders).
58+
59+
4960### Documentation
5061
5162#### In the code
ChangeLog.md+23
@@ -2,6 +2,29 @@
22
33This project's release branch is `master`. This log is written from the perspective of the release branch: when changes hit `master`, they are considered released, and the date should reflect that release.
44
5+## Unreleased
6+* Configs become ByteStrings.
7+* FrontendConfigsT has been changed into ConfigsT and configs are made available via getConfig/getConfigs
8+ * The frontend will still only have access to configs that are placed in config/frontend and config/common, while the backend has access to the entire contents of the config directory via `Obelisk.ExecutableConfig.Lookup.getConfigs`.
9+* The backend no longer runs in BackendConfigsT.
10+* Add tabulation package. See Data.Tabulation for details.
11+* Add encoders for `DMap`, `HasFields` (cf. Data.Tabulation), and JSON.
12+* Generalised pathSegmentEncoder, added pathFieldEncoder.
13+* Added some Prisms to the encoder library for manipulating DSums (perhaps they should get moved to dependent-sum before release?)
14+
15+## v0.1.1.0 - 2019-05-17
16+
17+* Fix crashes of Android apps on 32-bit ARM devices.
18+* Provide a way to indicate acceptance of the Android SDK license by passing `config.android_sdk.accept_license = true;` in the arguments to the import of `.obelisk/impl` in the project's `default.nix`.
19+* Add `COMPLETE` pragma to `(:/)`. Using this pattern synonym should no longer generate spurious warnings about non-exhaustive pattern matching.
20+* Make asset path hashing strict (see `Obelisk.Asset.Gather`)
21+* Add the `ob shell` command to enter a nix shell for an obelisk project
22+* Allow skeleton's obelisk to be overridden. This changes the skeleton's default.nix interface: the arguments that it used to take are now part of the new "obelisk" argument.
23+* Removed `MonadIO` from `ObeliskWidget` to prevent accidental IO during prerendering. If you need to do IO in a widget it should be on the right hand side of a `prerender`.
24+* Significantly changed the interface to the "executable config" packages. `obelisk-executable-config-lookup` is a new internal package which looks up all configs in a platform-specific way. `obelisk-executable-frontend` and `obelisk-executable-backend` provide MTL-style monad classes (`HasFrontendConfigs` and `HasBackendConfigs`) which the frontend and backend, respectively, can use to look up configs. This replaces the old `get` function which ran in `IO`.
25+* Add a flag to force thunk packing even if there are unpushed changes in the unpacked thunk.
26+
27+
528## v0.1.0.0 - 2019-03-29
629
730* Use reflex-dom's `HydrationDomBuilder` to "hydrate" statically rendered DOM served by the Obelisk backend (rather than re-creating DOM and replacing it all).
FAQ.md+76
@@ -2,6 +2,8 @@
22
331. [How do I fix invalid entitlements? ](#how-do-i-fix-invalid-entitlements)
441. [`ob thunk update` or `ob deploy update` fails](#ob-thunk-update-or-ob-deploy-update-fails)
5+1. [How do I fix `Ambiguous module name` errors?](#how-do-i-fix-ambiguous-module-name-errors)
6+1. [What does `ob run` actually do?](#what-does-ob-run-actually-do)
57
68### How do I fix invalid entitlements?
79
@@ -44,3 +46,77 @@ ob thunk pack $SOME_THUNK
4446Substitute the directory of your thunk for `$SOME_THUNK`. In the case of `ob deploy update`, `$SOME_THUNK` will be `src` in the deployment directory.
4547
4648(Based on issue #351).
49+
50+### How do I fix `Ambiguous module name` errors?
51+Since obelisk places the common/backend/frontend modules packages into the same ghci, and ghci doesn't "sandbox" them, it is possible to have conflicting module errors inside `ob repl/watch/run` that do not appear when doing a cabal build.
52+
53+You can disambiguate this via [PackageImports](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/glasgow_exts.html#package-qualified-imports). For instance, if you see
54+```
55+error:
56+ Ambiguous module name ‘Crypto.Hash’:
57+ it was found in multiple packages:
58+ cryptohash-0.11.9 cryptonite-0.25
59+```
60+then specify the package you want in the import, e.g:
61+`import "cryptonite" Crypto.Hash`
62+
63+
64+### What does `ob run` actually do?
65+
66+
67+#### Short version:
68+
69+`ob run` starts a [`ghcid`](https://github.com/ndmitchell/ghcid) process which
70+tries to build your project within a carefully crafted `nix-shell` with all the
71+project's dependencies and,
72+
73+* either displays compilation errors/warnings,
74+* or starts the Obelisk server, which serves:
75+ * your backend's route handlers,
76+ * the static assets,
77+ * the [JSaddle](https://github.com/ghcjs/jsaddle) frontend code (while opening its websocket).
78+
79+#### Longer version:
80+
81+Assuming we are in a project created with `ob init`, `ob run` calls (see
82+`lib/command/src/Obelisk/Command.hs`):
83+
84+ nix-shell -A shells.ghc --run 'ob --no-handoff internal run-static-io <real-run-function>'
85+
86+where
87+
88+* `shells.ghc` is defined in `./default.nix` by importing `./.obelisk/impl/default.nix` which
89+ is `default.nix` in the present (Obelisk) repository,
90+* `run-static-io` is a logging-enabled command wrapper
91+ (cf. `runObelisk` in `lib/command/src/Obelisk/App.hs`).
92+
93+In this case, it runs the function `Obelisk.Command.Command.run` (defined in
94+`lib/command/src/Obelisk/Command/Run.hs`).
95+
96+* It creates a GHCi config from a Nix expression which:
97+ * loads three packages: `backend`, `common`, `frontend`,
98+ * obtains a free port number.
99+* Then runs `ghcid`
100+ * with a command that reruns `Obelisk.Run.run` at each restart (option
101+ `--test`).
102+
103+It is defined in `lib/run/src/Obelisk/Run.hs`:
104+
105+* It creates a thread which starts the main backend.
106+ This runs `runSnapWithCommandLineArgs` and passes routes:
107+ * to the backend (result of the `_backend_run` field of your
108+ implementation of the `Backend fullRoute frontendRoute` record),
109+ * or to `serveDefaultObeliskApp` (`lib/backend/src/Obelisk/Backend.hs`) to
110+ serve static assets.
111+* Starts `runWidget`
112+ which itself runs
113+ [runSettingsSocket](https://hackage.haskell.org/package/warp-3.2.26/docs/Network-Wai-Handler-Warp.html#v:runSettingsSocket)
114+ (the *“TCP listen loop”*):
115+ * it binds to TCP socket, and
116+ * creates the HTTP connection manager
117+ ( [`newManager`](https://hackage.haskell.org/package/http-client-0.6.3/docs/Network-HTTP-Client.html#v:newManager))
118+ * calls `obeliskApp` which
119+ * has a route to serve the [JSaddle](https://github.com/ghcjs/jsaddle) frontend,
120+ * runs the frontend (*on* the server), to produce pre-rendered pages,
121+ * starts the JSaddle web-socket,
122+ * “falls back” to proxying the backend (`fallbackProxy`).
README.md+3−1
@@ -24,6 +24,7 @@ Obelisk provides an easy way to develop and deploy your [Reflex](https://github.
2424 nix.binaryCaches = [ "https://cache.nixos.org/" "https://nixcache.reflex-frp.org" ];
2525 nix.binaryCachePublicKeys = [ "ryantrinkle.com-1:JJiAKaRv9mWgpVAz8dwewnZe0AzzEAzPkagE9SP5NWI=" ];
2626 ```
27+ and rebuild your NixOS configuration (e.g. `sudo nixos-rebuild switch`).
2728 1. If you are using another operating system or linux distribution, ensure that these lines are present in your Nix configuration file (`/etc/nix/nix.conf` on most systems; [see full list](https://nixos.org/nix/manual/#sec-conf-file)):
2829 ```
2930 substituters = https://cache.nixos.org https://nixcache.reflex-frp.org
@@ -81,7 +82,7 @@ Obelisk leverages ghcid to provide a live-reloading server that handles both fro
8182ob run
8283```
8384
84−Now go to http://localhost:8000 (or the port specified in `config/common/route`) to access your app.
85+Now go to http://localhost:8000 (or the address/port specified in `config/common/route`) to access your app.
8586
8687Every time you change the Haskell source files in frontend, common or backend, `ob run` will automatically recompile the modified files and reload the server. Furthermore, it will display on screen compilation errors and warnings if any.
8788
@@ -272,6 +273,7 @@ It's also possible to inspect iOS WkWebView apps once they are installed in the
272273NOTE: Currently Android builds are only supported on Linux.
273274
2742751. In your project's `default.nix` set a suitable value for `android.applicationId` and `android.displayName`.
276+1. In your project's `default.nix` pass `config.android_sdk.accept_license = true;` in the arguments to the import of of `.obelisk/impl` to indicate your acceptance of the [Android Software Development Kit License Agreement](https://developer.android.com/studio/terms), which is required to build Android apps.
2752771. Run `nix-build -A android.frontend -o result-android` to build the Android app.
2762781. A debug version of the app should be generated at `result-android/android-app-debug.apk`
277279
default.nix+20−21
@@ -1,6 +1,7 @@
11{ system ? builtins.currentSystem
22, profiling ? false
33, iosSdkVersion ? "10.2"
4+, config ? {}
45}:
56let
67 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -16,7 +17,7 @@ let
1617 ];
1718
1819 getReflexPlatform = sys: import ./dep/reflex-platform {
19− inherit iosSdkVersion;
20+ inherit iosSdkVersion config;
2021 system = sys;
2122 enableLibraryProfiling = profiling;
2223
@@ -38,21 +39,14 @@ let
3839 rev = "42afdc21da5d9e076eab57eaa42bfdde938192b8";
3940 sha256 = "0psw384dx9bw2dp93xrzw8rd9amvcwgzn64jzzwby7sfspj6k349";
4041 }) {});
41− # Need 8.0.2 build support
42− # PR: https://github.com/dmwit/universe/pull/33
43− universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
44− owner = "obsidiansystems";
45− repo = "universe";
46− rev = "6a71119bfa5db2b9990a2491c941469ff8ef5d13";
47− sha256 = "0z8smyainnlzcglv3dlx6x1n9j6d2jv48aa8f2421iayfkxg3js5";
48− } + /template) {};
4942 })
5043
44+ pkgs.obeliskExecutableConfig.haskellOverlay
45+
5146 # Add obelisk packages
5247 (self: super: let
5348 pkgs = self.callPackage ({ pkgs }: pkgs) {};
5449 in {
55− obelisk-executable-config = pkgs.obeliskExecutableConfig.haskellPackage self;
5650 obelisk-executable-config-inject = pkgs.obeliskExecutableConfig.platforms.web.inject self;
5751
5852 obelisk-asset-manifest = self.callCabal2nix "obelisk-asset-manifest" (hackGet ./lib/asset + "/manifest") {};
@@ -65,6 +59,7 @@ let
6559 obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
6660 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
6761 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
62+ tabulation = self.callCabal2nix "tabulation" (cleanSource ./lib/tabulation) {};
6863 })
6964
7065 (self: super: let
@@ -162,12 +157,16 @@ in rec {
162157 obelisk-asset-manifest-generate "$src" "$haskellManifest" ${packageName} ${moduleName} "$symlinked"
163158 '';
164159
165− compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
160+ compressedJs = frontend: optimizationLevel: pkgs.runCommand "compressedJs" {} ''
166161 mkdir $out
167162 cd $out
168163 ln -s "${haskellLib.justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
169− 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
170− 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+ ''}
171170 '';
172171
173172 serverModules = {
@@ -273,7 +272,7 @@ in rec {
273272 , tools ? _: []
274273 , shellToolOverrides ? _: _: {}
275274 , withHoogle ? false # Setting this to `true` makes shell reloading far slower
276− , __closureCompilerOptimizationLevel ? "ADVANCED"
275+ , __closureCompilerOptimizationLevel ? "ADVANCED" # Set this to `null` to skip the closure-compiler step
277276 }:
278277 let frontendName = "frontend";
279278 backendName = "backend";
@@ -298,21 +297,21 @@ in rec {
298297 injectableConfig = builtins.filterSource (path: _:
299298 !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
300299 );
301− __android = configPath: {
300+ __androidWithConfig = configPath: {
302301 ${if android == null then null else frontendName} = {
303302 executableName = "frontend";
304303 ${if builtins.pathExists staticFiles then "assets" else null} =
305304 nixpkgs.obeliskExecutableConfig.platforms.android.inject
306− (if configPath == null then null else injectableConfig configPath)
305+ (injectableConfig configPath)
307306 processedStatic.symlinked;
308307 } // android;
309308 };
310− __ios = configPath: {
309+ __iosWithConfig = configPath: {
311310 ${if ios == null then null else frontendName} = {
312311 executableName = "frontend";
313312 ${if builtins.pathExists staticFiles then "staticSrc" else null} =
314313 nixpkgs.obeliskExecutableConfig.platforms.ios.inject
315− (if configPath == null then null else injectableConfig configPath)
314+ (injectableConfig configPath)
316315 processedStatic.symlinked;
317316 } // ios;
318317 };
@@ -335,9 +334,9 @@ in rec {
335334 commonName
336335 ];
337336 };
338− android = __android null;
339− ios = __ios null;
340− passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __ios __android; };
337+ android = __androidWithConfig (base + "/config");
338+ ios = __iosWithConfig (base + "/config");
339+ passthru = { inherit android ios packages overrides tools shellToolOverrides withHoogle staticFiles staticFilesImpure __closureCompilerOptimizationLevel processedStatic __iosWithConfig __androidWithConfig; };
341340 };
342341 in mkProject (projectDefinition args));
343342 serverOn = sys: version: serverExe
dep/reflex-platform/github.json+3−3
@@ -1,7 +1,7 @@
11{
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
4− "branch": "develop",
5− "rev": "f86e8363ab1a5068b8419a61856e1aefea952bb0",
6− "sha256": "1f5qhrkj9h07j5xzw8s10x0r7b19vbh8j9z9lbzy23yxwmj78y7f"
4+ "branch": "cg-bump-reflex-dom",
5+ "rev": "b220ba089e2de2110eb22c5388e09d99e4321148",
6+ "sha256": "0ds6bcw4p9q5x8h39djyqp5sp7wdzr9mmknsyjafn5l199hscppw"
77}
jobsets.nix+5
@@ -29,6 +29,11 @@ let
2929 emailresponsible = false;
3030 };
3131 };
32+ self-args = {
33+ type = "nix";
34+ value = "{ config.android_sdk.accept_license = true; }";
35+ emailresponsible = false;
36+ };
3237 };
3338 branchJobset = branch: defaults {
3439 description = "obelisk-${branch}";
lib/asset/manifest/obelisk-asset-manifest.cabal+1
@@ -17,6 +17,7 @@ library
1717 base
1818 , bytestring
1919 , containers
20+ , deepseq
2021 , directory
2122 , SHA
2223 , filepath
lib/asset/manifest/src/Obelisk/Asset/Gather.hs+5−2
@@ -1,10 +1,12 @@
11{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE BangPatterns #-}
23-- | Functions for collecting up files to be processed by the asset pipeline
34module Obelisk.Asset.Gather
45 ( gatherHashedPaths
56 , toHashedPath
67 ) where
78
9+import Control.DeepSeq
810import Control.Monad (forM)
911import Data.Bits
1012import qualified Data.ByteString.Lazy as LBS
@@ -33,7 +35,7 @@ gatherHashedPaths root = go ""
3335 let relativePath = subdir </> sub
3436 isFile <- doesFileExist $ root </> relativePath
3537 if isFile
36− then do hashedRelativePath <- toHashedPath root relativePath
38+ then do !hashedRelativePath <- force <$> toHashedPath root relativePath
3739 return $ Map.singleton relativePath hashedRelativePath
3840 else go relativePath
3941
@@ -47,7 +49,8 @@ toHashedPath root relativePath = do
4749 contents <- LBS.readFile path
4850 let hashPrefix = T.unpack $ decodeUtf8 $ LBS.toStrict $ toNixBase32 $ bytestringDigest $ sha256 contents
4951 (dir, filename) = splitFileName relativePath
50− return $ normalise $ dir </> (hashPrefix <> "-" <> filename)
52+ !hashedRelativePath = force $ normalise $ dir </> (hashPrefix <> "-" <> filename)
53+ return hashedRelativePath
5154
5255-- | Convert a ByteString to base 32 in the way that Nix does
5356toNixBase32 :: LBS.ByteString -> LBS.ByteString
lib/backend/obelisk-backend.cabal+3
@@ -8,6 +8,7 @@ library
88 build-depends: base,
99 bytestring,
1010 categories,
11+ containers,
1112 cookie,
1213 data-default,
1314 dependent-sum,
@@ -15,6 +16,8 @@ library
1516 lens,
1617 mtl,
1718 obelisk-asset-serve-snap,
19+ obelisk-executable-config-inject,
20+ obelisk-executable-config-lookup,
1821 obelisk-frontend,
1922 obelisk-route,
2023 obelisk-snap-extras,
lib/backend/src/Obelisk/Backend.hs+42−31
@@ -19,6 +19,7 @@ module Obelisk.Backend
1919 , runBackend
2020 , staticRenderContentType
2121 , mkRouteToUrl
22+ , getPublicConfigs
2223 ) where
2324
2425import Prelude hiding (id, (.))
@@ -30,13 +31,15 @@ import Data.ByteString (ByteString)
3031import qualified Data.ByteString.Char8 as BSC8
3132import Data.Default (Default (..))
3233import Data.Dependent.Sum
33−import Data.Functor.Sum
3434import Data.Functor.Identity
35+import Data.Map (Map)
36+import qualified Data.Map as Map
3537import Data.Monoid ((<>))
3638import Data.Text (Text)
3739import qualified Data.Text as T
3840import Data.Text.Encoding
3941import Obelisk.Asset.Serve.Snap (serveAsset)
42+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4043import Obelisk.Frontend
4144import Obelisk.Route
4245import Obelisk.Snap.Extras (doNotCache, serveFileIfExistsAs)
@@ -48,7 +51,7 @@ import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog
4851import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
4952
5053data Backend backendRoute frontendRoute = Backend
51− { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
54+ { _backend_routeEncoder :: Encoder (Either Text) Identity (R (FullRoute backendRoute frontendRoute)) PageName
5255 , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
5356 }
5457
@@ -67,9 +70,10 @@ serveDefaultObeliskApp
6770 -> ([Text]
6871 -> m ())
6972 -> Frontend (R appRoute)
73+ -> Map Text ByteString
7074 -> R (ObeliskRoute appRoute)
7175 -> m ()
72−serveDefaultObeliskApp urlEnc serveStaticAsset frontend = serveObeliskApp urlEnc serveStaticAsset frontendApp
76+serveDefaultObeliskApp urlEnc serveStaticAsset frontend configs = serveObeliskApp urlEnc serveStaticAsset frontendApp configs
7377 where frontendApp = GhcjsApp
7478 { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
7579 , _ghcjsApp_value = frontend
@@ -94,26 +98,22 @@ defaultFrontendGhcjsAssets = StaticAssets
9498 , _staticAssets_unprocessed = "frontend.jsexe"
9599 }
96100
97−runSnapWithCommandLineArgs :: Snap () -> IO ()
101+runSnapWithCommandLineArgs :: MonadIO m => Snap () -> m ()
98102runSnapWithCommandLineArgs a = do
99103 -- Get the web server configuration from the command line
100− cmdLineConf <- commandLineConfig defaultConfig
104+ cmdLineConf <- liftIO $ commandLineConfig defaultConfig
101105 let httpConf = cmdLineConf
102106 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
103107 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
104108 }
105109 -- Start the web server
106− httpServe httpConf a
110+ liftIO $ httpServe httpConf a
107111
108112getPageName :: (MonadSnap m) => m PageName
109113getPageName = do
110114 p <- getsRequest rqPathInfo
111115 q <- getsRequest rqQueryString
112− let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
113− pageNameEncoder' = bimap
114− (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
115− (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
116− return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
116+ return $ byteStringsToPageName p q
117117
118118getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
119119getRouteWith e = do
@@ -125,13 +125,14 @@ serveObeliskApp
125125 => (R appRoute -> Text)
126126 -> ([Text] -> m ())
127127 -> GhcjsApp (R appRoute)
128+ -> Map Text ByteString
128129 -> R (ObeliskRoute appRoute)
129130 -> m ()
130−serveObeliskApp urlEnc serveStaticAsset frontendApp = \case
131− ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
131+serveObeliskApp urlEnc serveStaticAsset frontendApp config = \case
132+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
132133 ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
133134 ResourceRoute_Static :=> Identity pathSegments -> serveStaticAsset pathSegments
134− ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
135+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp urlEnc frontendApp config $ GhcjsAppRoute_Resource :/ pathSegments
135136 ResourceRoute_JSaddleWarp :=> Identity _ -> do
136137 let msg = "Error: Obelisk.Backend received jsaddle request"
137138 liftIO $ putStrLn $ T.unpack msg
@@ -159,43 +160,53 @@ serveGhcjsApp
159160 :: (MonadSnap m, HasCookies m)
160161 => (R appRouteComponent -> Text)
161162 -> GhcjsApp (R appRouteComponent)
163+ -> Map Text ByteString
162164 -> R (GhcjsAppRoute appRouteComponent)
163165 -> m ()
164−serveGhcjsApp urlEnc app = \case
166+serveGhcjsApp urlEnc app config = \case
165167 GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest -> do
166168 modifyResponse $ setContentType staticRenderContentType
167− writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
169+ writeBS <=< renderGhcjsFrontend urlEnc (appRouteComponent :/ appRouteRest) config $ _ghcjsApp_value app
168170 GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
169171
170−runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
172+runBackend :: Backend backendRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
171173runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
172174 Left e -> fail $ "backend error:\n" <> T.unpack e
173− Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
174− runSnapWithCommandLineArgs $ do
175− getRouteWith validFullEncoder >>= \case
176− Identity r -> case r of
177− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
178− InR obeliskRoute :=> Identity a ->
179− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend $ obeliskRoute :/ a
180−
181−mkRouteToUrl :: Encoder Identity parse (R (Sum f (ObeliskRoute r))) PageName -> R r -> Text
175+ Right validFullEncoder -> do
176+ publicConfigs <- getPublicConfigs
177+ _backend_run backend $ \serveRoute -> do
178+ runSnapWithCommandLineArgs $ do
179+ getRouteWith validFullEncoder >>= \case
180+ Identity r -> case r of
181+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
182+ FullRoute_Frontend obeliskRoute :/ a ->
183+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) (serveStaticAssets defaultStaticAssets) frontend publicConfigs $
184+ obeliskRoute :/ a
185+
186+mkRouteToUrl :: Encoder Identity parse (R (FullRoute br fr)) PageName -> R fr -> Text
182187mkRouteToUrl validFullEncoder =
183188 let pageNameEncoder' :: Encoder Identity (Either Text) PageName PathQuery = pageNameEncoder
184− in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (InR $ ObeliskRoute_App k) :/ v
189+ in \(k :/ v) -> T.pack . uncurry (<>) . encode pageNameEncoder' . encode validFullEncoder $ (FullRoute_Frontend $ ObeliskRoute_App k) :/ v
185190
186191renderGhcjsFrontend
187192 :: (MonadSnap m, HasCookies m)
188193 => (route -> Text)
189194 -> route
195+ -> Map Text ByteString
190196 -> Frontend route
191197 -> m ByteString
192−renderGhcjsFrontend urlEnc route f = do
198+renderGhcjsFrontend urlEnc route configs f = do
193199 let ghcjsPreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "ghcjs/all.js") blank
194200 ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
195201 cookies <- askCookies
196− renderFrontendHtml cookies urlEnc route
197− (_frontend_head f >> ghcjsPreload)
198− (_frontend_body f >> ghcjsScript)
202+ renderFrontendHtml configs cookies urlEnc route f ghcjsPreload ghcjsScript
199203
200204instance HasCookies Snap where
201205 askCookies = map (\c -> (cookieName c, cookieValue c)) <$> getsRequest rqCookies
206+
207+-- | Get configs from the canonical "public" locations (i.e., locations that obelisk expects to make available
208+-- to frontend applications, and hence visible to end users).
209+getPublicConfigs :: IO (Map Text ByteString)
210+getPublicConfigs = Map.filterWithKey (\k _ -> isMemberOf k ["common", "frontend"]) <$> Lookup.getConfigs
211+ where
212+ isMemberOf k = any (`T.isPrefixOf` k)
lib/command/src/Obelisk/Command.hs+50−5
@@ -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
@@ -92,6 +93,8 @@ data ObCommand
9293 | ObCommand_Thunk ThunkCommand
9394 | ObCommand_Repl
9495 | ObCommand_Watch
96+ | ObCommand_Shell ShellOpts
97+ | ObCommand_Doc String [String] -- shell and list of packages
9598 | ObCommand_Internal ObInternal
9699 deriving Show
97100
@@ -103,7 +106,7 @@ data ObInternal
103106inNixShell' :: MonadObelisk m => StaticPtr (ObeliskT IO ()) -> m ()
104107inNixShell' p = withProjectRoot "." $ \root -> do
105108 cmd <- liftIO $ unwords <$> mkCmd -- TODO: shell escape instead of unwords
106− projectShell root False "ghc" cmd
109+ projectShell root False "ghc" (Just cmd)
107110 where
108111 mkCmd = do
109112 argsCfg <- getArgsConfig
@@ -127,6 +130,12 @@ obCommand cfg = hsubparser
127130 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
128131 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
129132 , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
133+ , 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")
130139 ])
131140 <|> subparser
132141 (mconcat
@@ -134,6 +143,9 @@ obCommand cfg = hsubparser
134143 , command "internal" (info (ObCommand_Internal <$> internalCommand) mempty)
135144 ])
136145
146+packageNames :: Parser [String]
147+packageNames = some (strArgument (metavar "PACKAGE-NAME..."))
148+
137149deployCommand :: ArgsConfig -> Parser DeployCommand
138150deployCommand cfg = hsubparser $ mconcat
139151 [ command "init" $ info (DeployCommand_Init <$> deployInitOpts) $ progDesc "Initialize a deployment configuration directory"
@@ -211,16 +223,37 @@ thunkDirectoryParser = fmap (dropTrailingPathSeparator . normalise) . strArgumen
211223data ThunkCommand
212224 = ThunkCommand_Update [FilePath] (Maybe String)
213225 | ThunkCommand_Unpack [FilePath]
214− | ThunkCommand_Pack [FilePath]
226+ | ThunkCommand_Pack [FilePath] Bool
215227 deriving Show
216228
229+forceFlag :: Parser Bool
230+forceFlag = switch $ long "force" <> short 'f' <> help "Force packing thunks even if there are branches not pushed upstream, uncommitted changes, stashes. This will cause changes that have not been pushed upstream to be lost; use with care."
231+
217232thunkCommand :: Parser ThunkCommand
218233thunkCommand = hsubparser $ mconcat
219234 [ command "update" $ info (ThunkCommand_Update <$> some thunkDirectoryParser <*> optional (strOption (long "branch" <> metavar "BRANCH"))) $ progDesc "Update thunk to latest revision available"
220235 , command "unpack" $ info (ThunkCommand_Unpack <$> some thunkDirectoryParser) $ progDesc "Unpack thunk into git checkout of revision it points to"
221− , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
236+ , command "pack" $ info (ThunkCommand_Pack <$> some thunkDirectoryParser <*> forceFlag) $ progDesc "Pack git checkout into thunk that points at the current branch's upstream"
222237 ]
223238
239+data ShellOpts
240+ = ShellOpts
241+ { _shellOpts_shell :: String
242+ , _shellOpts_command :: Maybe String
243+ }
244+ deriving Show
245+
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+
252+shellOpts :: Parser ShellOpts
253+shellOpts = ShellOpts
254+ <$> shellFlags
255+ <*> optional (strArgument (metavar "COMMAND"))
256+
224257parserPrefs :: ParserPrefs
225258parserPrefs = defaultPrefs
226259 { prefShowHelpOnEmpty = True
@@ -325,7 +358,7 @@ ob = \case
325358 Right (ThunkData_Packed ptr) -> return ptr
326359 Right (ThunkData_Checkout (Just ptr)) -> return ptr
327360 Right (ThunkData_Checkout Nothing) ->
328− getThunkPtr' False root
361+ getThunkPtr False root
329362 let sshKeyPath = _deployInitOpts_sshKey deployOpts
330363 hostname = _deployInitOpts_hostname deployOpts
331364 route = _deployInitOpts_route deployOpts
@@ -344,9 +377,13 @@ ob = \case
344377 ObCommand_Thunk tc -> case tc of
345378 ThunkCommand_Update thunks mBranch -> mapM_ ((flip updateThunkToLatest) mBranch) thunks
346379 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
347− ThunkCommand_Pack thunks -> forM_ thunks packThunk
380+ ThunkCommand_Pack thunks force -> forM_ thunks (packThunk force)
348381 ObCommand_Repl -> runRepl
349382 ObCommand_Watch -> inNixShell' $ static runWatch
383+ ObCommand_Shell so -> withProjectRoot "." $ \root ->
384+ projectShell root False (_shellOpts_shell so) (_shellOpts_command so)
385+ ObCommand_Doc shell pkgs -> withProjectRoot "." $ \root ->
386+ projectShell root False shell (Just $ haddockCommand pkgs)
350387 ObCommand_Internal icmd -> case icmd of
351388 ObInternal_RunStaticIO k -> liftIO (unsafeLookupStaticPtr @(ObeliskT IO ()) k) >>= \case
352389 Nothing -> failWith $ "ObInternal_RunStaticIO: no such StaticKey: " <> T.pack (show k)
@@ -355,6 +392,14 @@ ob = \case
355392 liftIO $ runObelisk c $ deRefStaticPtr p
356393 ObInternal_CLIDemo -> cliDemo
357394
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+
358403--TODO: Clean up all the magic strings throughout this codebase
359404
360405getArgsConfig :: IO ArgsConfig
lib/command/src/Obelisk/Command/Deploy.hs+3−3
@@ -108,7 +108,7 @@ deployPush deployPath getNixBuilders = do
108108 Right (ThunkData_Packed ptr) -> return ptr
109109 Right (ThunkData_Checkout _) -> do
110110 checkGitCleanStatus srcPath True >>= \case
111− True -> packThunk srcPath
111+ True -> packThunk False srcPath
112112 False -> failWith $ T.pack $ "ob deploy push: ensure " <> srcPath <> " has no pending changes and latest is pushed upstream."
113113 Left err -> failWith $ "ob deploy push: couldn't read src thunk: " <> T.pack (show err)
114114 let version = show . _thunkRev_commit $ _thunkPtr_rev thunkPtr
@@ -234,7 +234,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
234234 [ "with (import ", srcDir, " {});"
235235 , "android.frontend.override (drv: { "
236236 , "releaseKey = (if builtins.isNull drv.releaseKey then {} else drv.releaseKey) // " <> releaseKey <> "; "
237− , "staticSrc = (passthru.__android ", configDir, ").frontend.staticSrc;"
237+ , "staticSrc = (passthru.__androidWithConfig ", configDir, ").frontend.staticSrc;"
238238 , "})"
239239 ]
240240 return $ Target
@@ -245,7 +245,7 @@ deployMobile platform mobileArgs = withProjectRoot "." $ \root -> do
245245 IOS -> do
246246 let expr = mconcat
247247 [ "with (import ", srcDir, " {});"
248− , "ios.frontend.override (_: { staticSrc = (passthru.__ios ", configDir, ").frontend.staticSrc; })"
248+ , "ios.frontend.override (_: { staticSrc = (passthru.__iosWithConfig ", configDir, ").frontend.staticSrc; })"
249249 ]
250250 return $ Target
251251 { _target_path = Nothing
lib/command/src/Obelisk/Command/Project.hs+6−5
@@ -196,20 +196,21 @@ isWritableOnlyBy s uid = fileOwner s == uid && fileMode s .&. 0o22 == 0
196196-- | Run a command in the given shell for the current project
197197inProjectShell :: MonadObelisk m => String -> String -> m ()
198198inProjectShell shellName command = withProjectRoot "." $ \root ->
199− projectShell root True shellName command
199+ projectShell root True shellName (Just command)
200200
201201inImpureProjectShell :: MonadObelisk m => String -> String -> m ()
202202inImpureProjectShell shellName command = withProjectRoot "." $ \root ->
203− projectShell root False shellName command
203+ projectShell root False shellName (Just command)
204204
205−projectShell :: MonadObelisk m => FilePath -> Bool -> String -> String -> m ()
205+projectShell :: 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" $
208208 [ "--pure" | isPure ] <>
209209 [ "-A"
210210 , "shells." <> shellName
211− , "--run", command
212− ]
211+ ] <> case command of
212+ Nothing -> []
213+ Just c -> ["--run", c]
213214 void $ liftIO $ waitForProcess ph
214215
215216setCtlc :: CreateProcess -> CreateProcess
lib/command/src/Obelisk/Command/Run.hs+15−2
@@ -10,8 +10,8 @@ import Control.Monad
1010import Control.Monad.IO.Class (liftIO)
1111import Control.Monad.Reader (MonadIO)
1212import Data.Either
13−import Data.List
14−import Data.List.NonEmpty as NE
13+import qualified Data.List as L
14+import qualified Data.List.NonEmpty as NE
1515import Data.Maybe
1616import qualified Data.Text as T
1717import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
@@ -47,6 +47,8 @@ data CabalPackageInfo = CabalPackageInfo
4747 -- ^ List of hs src dirs of the library component
4848 , _cabalPackageInfo_defaultExtensions :: [Extension]
4949 -- ^ List of globally enable extensions of the library component
50+ , _cabalPackageInfo_defaultLanguage :: Maybe Language
51+ -- ^ List of globally set languages of the library component
5052 }
5153
5254-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
@@ -140,6 +142,8 @@ parseCabalPackage dir = do
140142 fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
141143 , _cabalPackageInfo_defaultExtensions =
142144 defaultExtensions $ libBuildInfo lib
145+ , _cabalPackageInfo_defaultLanguage =
146+ defaultLanguage $ libBuildInfo lib
143147 }
144148 Left (_, errors) -> do
145149 putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
@@ -169,17 +173,22 @@ withGhciScript pkgs f = do
169173 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
170174
171175 let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
176+ languageFromPkgs = L.nub $ mapMaybe _cabalPackageInfo_defaultLanguage packageInfos
177+ -- NOTE when no default-language is present cabal sets Haskell98
178+ language = NE.toList $ fromMaybe (Haskell98 NE.:| []) $ NE.nonEmpty languageFromPkgs
172179 extensionsLine = if extensions == mempty
173180 then ""
174181 else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
175182 dotGhci = unlines $
176183 [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
177184 , extensionsLine
185+ , ":set " <> intercalate " " (("-X" <>) . prettyShow <$> language)
178186 , ":load Backend Frontend"
179187 , "import Obelisk.Run"
180188 , "import qualified Frontend"
181189 , "import qualified Backend"
182190 ]
191+ warnDifferentLanguages language
183192 withSystemTempDirectory "ob-ghci" $ \fp -> do
184193 let dotGhciPath = fp </> ".ghci"
185194 liftIO $ writeFile dotGhciPath dotGhci
@@ -189,6 +198,10 @@ withGhciScript pkgs f = do
189198 rootedSourceDirs pkg = NE.toList $
190199 (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
191200
201+warnDifferentLanguages :: MonadObelisk m => [Language] -> m ()
202+warnDifferentLanguages (_:_:_) = putLog Warning "Different languages detected across packages which may result in errors when loading the repl"
203+warnDifferentLanguages _ = return ()
204+
192205-- | Run ghci repl
193206runGhciRepl
194207 :: MonadObelisk m
lib/command/src/Obelisk/Command/Thunk.hs+7−12
@@ -26,7 +26,6 @@ module Obelisk.Command.Thunk
2626 , readThunk
2727 , updateThunk
2828 , getThunkPtr
29− , getThunkPtr'
3029 , parseGitUri
3130 , uriThunkPtr
3231 ) where
@@ -573,7 +572,7 @@ updateThunk p f = withSystemTempDirectory "obelisk-thunkptr-" $ \tmpDir -> do
573572 return tmpThunk
574573 Right _ -> failWith $ "Thunk is not packed"
575574 updateThunkFromTmp p' = do
576− _ <- packThunk' True p'
575+ _ <- packThunk' True False p'
577576 callProcessAndLogOutput (Notice, Error) $
578577 proc "cp" ["-r", "-T", p', p]
579578
@@ -622,28 +621,24 @@ unpackThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from with
622621 callProcessAndLogOutput (Notice, Error) $
623622 proc "mv" ["-T", tmpRepo, thunkDir]
624623
625−--TODO: add force mode to pack even if changes are present
626624--TODO: add a rollback mode to pack to the original thunk
627−packThunk :: MonadObelisk m => FilePath -> m ThunkPtr
625+packThunk :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
628626packThunk = packThunk' False
629627
630−packThunk' :: MonadObelisk m => Bool -> FilePath -> m ThunkPtr
631−packThunk' noTrail thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
628+packThunk' :: MonadObelisk m => Bool -> Bool -> FilePath -> m ThunkPtr
629+packThunk' noTrail force thunkDir = checkThunkDirectory "Can't pack/unpack from within the thunk directory" thunkDir >> readThunk thunkDir >>= \case
632630 Left err -> failWith $ T.pack $ "thunk pack: " <> show err
633631 Right (ThunkData_Packed _) -> failWith "pack: thunk is already packed"
634632 Right (ThunkData_Checkout _) -> do
635633 withSpinner' ("Packing thunk " <> T.pack thunkDir)
636634 (finalMsg noTrail $ const $ "Packed thunk " <> T.pack thunkDir) $ do
637− thunkPtr <- getThunkPtr thunkDir
635+ thunkPtr <- getThunkPtr (not force) thunkDir
638636 callProcessAndLogOutput (Debug, Error) $ proc "rm" ["-rf", thunkDir]
639637 liftIO $ createThunk thunkDir thunkPtr
640638 pure thunkPtr
641639
642−getThunkPtr :: MonadObelisk m => FilePath -> m ThunkPtr
643−getThunkPtr = getThunkPtr' True
644−
645−getThunkPtr' :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
646−getThunkPtr' checkClean thunkDir = do
640+getThunkPtr :: forall m. MonadObelisk m => Bool -> FilePath -> m ThunkPtr
641+getThunkPtr checkClean thunkDir = do
647642 when checkClean $ ensureCleanGitRepo thunkDir True $
648643 "thunk pack: thunk checkout contains unsaved modifications"
649644
lib/executable-config/default.nix+4−1
@@ -16,9 +16,12 @@ let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} (''
1616 exit 1
1717 fi
1818 cp -a "${config}"/* "$out/config"
19+ find "$out/config" -type f -printf '%P\0' >"$out/config.files"
1920 '');
2021in with nixpkgs.haskell.lib; {
21− haskellPackage = self: self.callPackage (filterGitSource ./lookup) {};
22+ haskellOverlay = self: super: {
23+ obelisk-executable-config-lookup = self.callPackage (filterGitSource ./lookup) {};
24+ };
2225 platforms = {
2326 android = {
2427 # Inject the given config directory into an android assets folder
lib/executable-config/inject/obelisk-executable-config-inject.cabal+2−2
@@ -9,8 +9,8 @@ library
99 build-depends:
1010 base,
1111 bytestring,
12− directory,
13− filepath,
12+ base64-bytestring,
13+ containers,
1414 reflex-dom-core,
1515 text
1616
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+14−40
@@ -2,50 +2,24 @@
22module Obelisk.ExecutableConfig.Inject where
33
44import Control.Monad (mapM_)
5−import Control.Monad.IO.Class (MonadIO, liftIO)
6−import Data.Semigroup ((<>))
5+import Control.Monad.IO.Class (MonadIO)
76import Data.ByteString (ByteString)
7+import qualified Data.ByteString.Base64 as B64
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Semigroup ((<>))
811import Data.Text (Text)
9−import qualified Data.Text as T
10−import qualified Data.Text.IO as T
12+import qualified Data.Text.Encoding as T
1113import Reflex.Dom.Core hiding (value)
12−import System.Directory
13−import System.FilePath ((</>))
14−
15−-- | Produces a @<script>@ tag containing the contents of the config item,
16−-- rendered as a 'ByteString'.
17−inject :: FilePath -> IO ByteString
18−inject key = do
19− value <- T.readFile key
20− fmap snd $ renderStatic $ injectPure (T.pack key) value
2114
2215-- | Produces a @<script>@ tag with an @id@ attribute containing the key and
23−-- whose value is the provided configuration
24−injectPure :: DomBuilder t m => Text -> Text -> m ()
16+-- whose value is the provided configuration, encoded in base64.
17+injectPure :: DomBuilder t m => Text -> ByteString -> m ()
2518injectPure key value =
26− let attrs = ("type" =: "text/plain" <> "id" =: ("config-" <> key))
27− in elAttr "script" attrs $ text value
28−
29−-- | Scans the "common" and "frontend" configuration folders and produces
30−-- injectable @<script>@ tags containing the configuration keys (filepaths) and
31−-- values.
32−injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => m ()
33−injectExecutableConfigs = do
34− cfgC <- getConfigs "config/common"
35− cfgF <- getConfigs "config/frontend"
36− mapM_ (uncurry injectPure) (cfgC <> cfgF)
19+ let attrs = ("type" =: "text/plain" <> "data-obelisk-executable-config-inject-key" =: key <> "data-hydration-skip" =: "")
20+ in elAttr "script" attrs $ text (T.decodeUtf8 (B64.encode value))
3721
38−getConfigs :: MonadIO m => FilePath -> m [(Text, Text)]
39−getConfigs fp = liftIO $ do
40− dir <- doesDirectoryExist fp
41− if dir
42− then do
43− ps <- listDirectory fp
44− fmap concat $ mapM (\p -> getConfigs $ fp </> p) ps
45− else do
46− file <- doesFileExist fp
47− if file
48− then do
49− f <- T.readFile fp
50− return [(T.pack fp, f)]
51− else return []
22+-- | Produces injectable @<script>@ tags containing the configuration keys
23+-- (filepaths) and values.
24+injectExecutableConfigs :: (MonadIO m, DomBuilder t m) => Map Text ByteString -> m ()
25+injectExecutableConfigs = mapM_ (uncurry injectPure) . Map.toList
lib/executable-config/lookup/default.nix+5−3
@@ -1,16 +1,18 @@
11{ hostPlatform, ghc
2−, mkDerivation, base, bytestring, filepath, stdenv, text, transformers
2+, mkDerivation, base, base64-bytestring, bytestring, filepath, stdenv, text, transformers
3+, transformers-base, ref-tf, primitive, monad-control, reflex, reflex-dom
4+, jsaddle
35, android-activity ? null, jsaddle-wkwebview ? null, ghcjs-dom ? null
46}:
57let isAndroid = hostPlatform.libc == "bionic";
68 isIOS = hostPlatform.isDarwin && hostPlatform.isAarch64;
79 isGhcjs = ghc.isGhcjs or false;
810in mkDerivation {
9− pname = "obelisk-executable-config";
11+ pname = "obelisk-executable-config-lookup";
1012 version = "0.1";
1113 src = ./.;
1214 libraryHaskellDepends = [
13− base bytestring filepath text transformers
15+ base bytestring filepath text transformers transformers-base ref-tf primitive monad-control reflex reflex-dom jsaddle
1416 ] ++ (if isAndroid then [
1517 android-activity
1618 ] else if isIOS then [
lib/executable-config/lookup/obelisk-executable-config-lookup.cabalnew file+45
@@ -0,0 +1,45 @@
1+name: obelisk-executable-config-lookup
2+version: 0.1.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ exposed-modules: Obelisk.ExecutableConfig.Lookup, Obelisk.Configs
8+ if os(ios)
9+ hs-source-dirs: src-ios, src
10+ other-modules: Obelisk.Configs.Internal.Directory
11+ build-depends: jsaddle-wkwebview
12+ build-depends: directory
13+ else
14+ if os(linux-android)
15+ hs-source-dirs: src-android, src
16+ c-sources: cbits/ExecutableConfig.c
17+ other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
18+ build-depends: android-activity
19+ ld-options: -landroid
20+ else
21+ if impl(ghcjs)
22+ hs-source-dirs: src-ghcjs, src
23+ build-depends: ghcjs-dom
24+ else
25+ hs-source-dirs: src-other, src
26+ other-modules: Obelisk.Configs.Internal.Directory
27+ build-depends: directory
28+
29+ build-depends:
30+ base,
31+ base64-bytestring,
32+ bytestring,
33+ containers,
34+ filepath,
35+ text,
36+ transformers,
37+ transformers-base,
38+ ref-tf,
39+ primitive,
40+ monad-control,
41+ reflex,
42+ reflex-dom,
43+ jsaddle
44+
45+ ghc-options: -Wall
lib/executable-config/lookup/obelisk-executable-config.cabaldeleted−30
@@ -1,30 +0,0 @@
1−name: obelisk-executable-config
2−version: 0.1.1
3−cabal-version: >= 1.2
4−build-type: Simple
5−
6−library
7− exposed-modules: Obelisk.ExecutableConfig
8− if os(ios)
9− hs-source-dirs: src-ios
10− build-depends: jsaddle-wkwebview
11− else
12− if os(linux-android)
13− hs-source-dirs: src-android
14− c-sources: cbits/ExecutableConfig.c
15− other-modules: Obelisk.ExecutableConfig.Internal.AssetManager
16− build-depends: android-activity
17− ld-options: -landroid
18− else
19− if impl(ghcjs)
20− hs-source-dirs: src-ghcjs
21− build-depends: ghcjs-dom
22− else
23− hs-source-dirs: src
24−
25− build-depends:
26− base,
27− bytestring,
28− filepath,
29− text,
30− transformers
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hsdeleted−27
@@ -1,27 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception (bracket)
4−import qualified Data.ByteString as BS
5−import Data.Text (Text)
6−import qualified Data.Text as T
7−import qualified Data.Text.Encoding as T
8−import Foreign.C.String (withCString)
9−import Foreign.Ptr (nullPtr)
10−import System.FilePath.Posix ((</>))
11−
12−import Obelisk.ExecutableConfig.Internal.AssetManager
13−
14−get :: Text -> IO (Maybe Text)
15−get name = bracket getAssets freeAssetManager $ \mgrObj -> do
16− mgr <- assetManagerFromJava mgrObj
17− let open = do
18− a <- withCString (T.unpack name) $ \fn ->
19− assetManager_open mgr fn 3
20− return $ if unAAsset a == nullPtr
21− then Nothing
22− else Just a
23− close = mapM_ asset_close
24− bracket open close $ mapM $ \asset -> do
25− b <- asset_getBuffer asset
26− l <- asset_getLength asset
27− fmap T.decodeUtf8 $ BS.packCStringLen (b, fromIntegral l)
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig/Lookup.hsnew file+60
@@ -0,0 +1,60 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Control.Exception (bracket)
6+import Control.Monad (forM)
7+import Data.ByteString (ByteString)
8+import qualified Data.ByteString as BS
9+import Data.Map (Map)
10+import qualified Data.Map as Map
11+import Data.Text (Text)
12+import qualified Data.Text.Encoding as T
13+import Foreign.C.String (withCString)
14+import Foreign.Ptr (nullPtr)
15+
16+import Obelisk.ExecutableConfig.Internal.AssetManager
17+
18+getFromMgr :: AAssetManager -> BS.ByteString -> IO (Maybe ByteString)
19+getFromMgr mgr name = do
20+ let open = do
21+ a <- BS.useAsCString name $ \fn ->
22+ assetManager_open mgr fn 3
23+ return $ if unAAsset a == nullPtr
24+ then Nothing
25+ else Just a
26+ close = mapM_ asset_close
27+ bracket open close $ mapM $ \asset -> do
28+ b <- asset_getBuffer asset
29+ l <- asset_getLength asset
30+ BS.packCStringLen (b, fromIntegral l)
31+
32+getConfigs :: IO (Map Text ByteString)
33+getConfigs = fmap (Map.mapKeys T.decodeUtf8) $ bracket getAssets freeAssetManager $ \mgrObj -> do
34+ mgr <- assetManagerFromJava mgrObj
35+ let openDir = do
36+ d <- withCString "config.files" $ \fn ->
37+ assetManager_open mgr fn 3
38+ return $ if unAAsset d == nullPtr
39+ then Nothing
40+ else Just d
41+ closeDir = mapM_ asset_close
42+ configPaths <- bracket openDir closeDir $ \case
43+ Just asset -> do
44+ b <- asset_getBuffer asset
45+ l <- asset_getLength asset
46+ lines0 <$> BS.packCStringLen (b, fromIntegral l)
47+ Nothing -> error "could not open configuration manifest 'config.files'"
48+ result <- fmap Map.fromList $ forM configPaths $ \fp ->
49+ getFromMgr mgr ("config/" <> fp) >>= \case
50+ Just v -> return (fp, v)
51+ Nothing -> error $ "Config present in config.files but not in assets: " <> show fp
52+ putStrLn $ "getConfigs: found " <> show result
53+ pure result
54+
55+lines0 :: BS.ByteString -> [BS.ByteString]
56+lines0 ps
57+ | BS.null ps = []
58+ | otherwise = case BS.elemIndex 0 ps of
59+ Nothing -> [ps]
60+ Just n -> BS.take n ps : lines0 (BS.drop (n+1) ps)
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig.hsdeleted−17
@@ -1,17 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−module Obelisk.ExecutableConfig where
3−
4−import Control.Monad.Trans.Maybe
5−import Data.Semigroup ((<>))
6−import Data.Text (Text)
7−import GHCJS.DOM
8−import GHCJS.DOM.Document
9−import GHCJS.DOM.Element
10−import GHCJS.DOM.NonElementParentNode
11−
12−get :: Text -> IO (Maybe Text)
13−get item = runMaybeT $ do
14− doc <- MaybeT currentDocument
15− e <- MaybeT $ getElementById doc $ "config-" <> item
16− getInnerHTML e
17−
lib/executable-config/lookup/src-ghcjs/Obelisk/ExecutableConfig/Lookup.hsnew file+42
@@ -0,0 +1,42 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.ByteString (ByteString)
6+import qualified Data.ByteString.Base64 as B64
7+import Data.Map (Map)
8+import qualified Data.Map as Map
9+import Data.Maybe (catMaybes)
10+import Data.Text (Text)
11+import qualified Data.Text.Encoding as T
12+import Data.Traversable (for)
13+import GHCJS.DOM
14+import GHCJS.DOM.Document (getHead)
15+import GHCJS.DOM.DOMStringMap (get)
16+import GHCJS.DOM.Element (getInnerHTML)
17+import GHCJS.DOM.HTMLElement (HTMLElement(HTMLElement), getDataset)
18+import GHCJS.DOM.NodeList (item, getLength)
19+import GHCJS.DOM.ParentNode (querySelectorAll)
20+import GHCJS.DOM.Types (Node(Node), castTo)
21+
22+getConfigs :: IO (Map Text ByteString)
23+getConfigs = do
24+ Just doc <- currentDocument
25+ Just hd <- getHead doc
26+ nodes <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
27+ fmap Map.fromList $ for nodes $ \node -> do
28+ e <- castTo HTMLElement node >>= \case
29+ Nothing -> error "Found node with data attribute obelisk-executable-config-inject-key that is not an HTMLElement."
30+ Just htmlE -> return htmlE
31+ dataset <- getDataset e
32+ (,)
33+ <$> get dataset ("obelisk-executable-config-inject-key" :: Text)
34+ <*> (fmap decodeOrFail (getInnerHTML e))
35+ where
36+ collToList es = do
37+ len <- getLength es
38+ list <- traverse (item es) $ if len == 0 then [] else [0..len-1]
39+ pure $ catMaybes list
40+ decodeOrFail x = case B64.decode (T.encodeUtf8 x) of
41+ Left e -> error ("Obelisk.ExecutableConfig.Lookup.getConfigs: error when decoding base64: " ++ e)
42+ Right x' -> x'
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hsdeleted−22
@@ -1,22 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Control.Monad
5−import Control.Monad.Trans.Maybe
6−import Data.ByteString as BS
7−import Data.Text (Text)
8−import qualified Data.Text as T
9−import qualified Data.Text.IO as T
10−import qualified Data.Text.Encoding as T
11−import Language.Javascript.JSaddle.WKWebView
12−import System.FilePath.Posix ((</>))
13−import System.IO.Error
14−
15−get :: Text -> IO (Maybe Text)
16−get name = fmap join $ mainBundleResourcePath >>= \mp -> forM mp $ \p ->
17− catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> T.unpack name
18−
19−catchDoesNotExist :: IO a -> IO (Maybe a)
20−catchDoesNotExist f = catchJust doesNotExist (Just <$> f) $ const $ return Nothing
21− where
22− doesNotExist e = if isDoesNotExistError e then Just () else Nothing
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig/Lookup.hsnew file+17
@@ -0,0 +1,17 @@
1+{-# LANGUAGE LambdaCase #-}
2+module Obelisk.ExecutableConfig.Lookup where
3+
4+import Data.ByteString (ByteString)
5+import Data.Map (Map)
6+import Data.Text (Text)
7+import qualified Data.Text as T
8+import qualified Data.Text.Encoding as T
9+import Language.Javascript.JSaddle.WKWebView
10+import System.FilePath.Posix
11+
12+import Obelisk.Configs.Internal.Directory
13+
14+getConfigs :: IO (Map Text ByteString)
15+getConfigs = mainBundleResourcePath >>= \case
16+ Nothing -> error "Could not get bundle resource path."
17+ Just p -> getConfigsFromDirectory $ T.unpack (T.decodeUtf8 p) </> "config"
lib/executable-config/lookup/src-other/Obelisk/ExecutableConfig/Lookup.hsnew file+18
@@ -0,0 +1,18 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.ExecutableConfig.Lookup where
4+
5+import Data.Map (Map)
6+import qualified Data.Map as Map
7+import Data.Text (Text)
8+import qualified Data.Text as T
9+import Data.ByteString (ByteString)
10+import qualified Data.ByteString as BS
11+import System.Directory
12+import System.FilePath
13+import Control.Monad
14+
15+import Obelisk.Configs.Internal.Directory
16+
17+getConfigs :: IO (Map Text ByteString)
18+getConfigs = getConfigsFromDirectory "config"
lib/executable-config/lookup/src/Obelisk/Configs.hsnew file+128
@@ -0,0 +1,128 @@
1+{-# LANGUAGE CPP #-}
2+{-# LANGUAGE DataKinds #-}
3+{-# LANGUAGE DefaultSignatures #-}
4+{-# LANGUAGE DeriveFunctor #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
8+{-# LANGUAGE MultiParamTypeClasses #-}
9+{-# LANGUAGE OverloadedStrings #-}
10+{-# LANGUAGE RankNTypes #-}
11+{-# LANGUAGE TypeFamilies #-}
12+{-# LANGUAGE UndecidableInstances #-}
13+module Obelisk.Configs
14+ ( HasConfigs(..)
15+ , ConfigsT
16+ , runConfigsT
17+ , mapConfigsT
18+ ) where
19+
20+import Control.Applicative
21+import Control.Monad
22+import Control.Monad.Base
23+import Control.Monad.Fix
24+import Control.Monad.Primitive
25+import Control.Monad.Ref
26+import Control.Monad.IO.Class
27+import Control.Monad.Trans.Class
28+import Control.Monad.Trans.Control
29+import Control.Monad.Trans.Reader
30+import Data.ByteString (ByteString)
31+import Data.Map (Map)
32+import qualified Data.Map as Map
33+import Data.Text (Text)
34+import qualified Data.Text.Encoding as T
35+import Reflex
36+import Reflex.Host.Class
37+import Reflex.Dom
38+#ifndef ghcjs_HOST_OS
39+import Language.Javascript.JSaddle (MonadJSM)
40+#endif
41+
42+class Monad m => HasConfigs m where
43+ getConfigs :: m (Map Text ByteString)
44+ default getConfigs :: (HasConfigs m', m ~ t m', MonadTrans t) => m (Map Text ByteString)
45+ getConfigs = lift getConfigs
46+ getConfig :: Text -> m (Maybe ByteString)
47+ getConfig k = do
48+ configs <- getConfigs
49+ return $ Map.lookup k configs
50+
51+instance Monad m => HasConfigs (ConfigsT m) where
52+ getConfigs = ConfigsT ask
53+
54+getTextConfig :: HasConfigs m => Text -> m (Maybe Text)
55+getTextConfig k = fmap T.decodeUtf8 <$> getConfig k
56+
57+instance HasConfigs m => HasConfigs (BehaviorWriterT t w m)
58+instance HasConfigs m => HasConfigs (DynamicWriterT t w m)
59+instance HasConfigs m => HasConfigs (EventWriterT t w m)
60+instance HasConfigs m => HasConfigs (PostBuildT t m)
61+instance HasConfigs m => HasConfigs (QueryT t q m)
62+instance HasConfigs m => HasConfigs (ReaderT r m)
63+instance HasConfigs m => HasConfigs (RequesterT t request response m)
64+instance HasConfigs m => HasConfigs (StaticDomBuilderT t m)
65+instance HasConfigs m => HasConfigs (TriggerEventT t m)
66+
67+newtype ConfigsT m a = ConfigsT { unConfigsT :: ReaderT (Map Text ByteString) m a }
68+ deriving
69+ ( Functor
70+ , Applicative
71+ , Monad
72+ , MonadPlus
73+ , Alternative
74+ , MonadFix
75+ , MonadIO
76+ , MonadBase m'
77+ , MonadBaseControl m'
78+ , MonadRef
79+ , MonadTrans
80+ , DomBuilder t
81+ , MonadHold t
82+ , MonadReflexCreateTrigger t
83+ , MonadSample t
84+ , NotReady t
85+ , PostBuild t
86+ , TriggerEvent t
87+ , HasDocument
88+ , DomRenderHook t
89+ , HasJSContext
90+ , HasJS js
91+#ifndef ghcjs_HOST_OS
92+ , MonadJSM
93+#endif
94+ )
95+
96+instance PerformEvent t m => PerformEvent t (ConfigsT m) where
97+ type Performable (ConfigsT m) = ConfigsT (Performable m)
98+ performEvent e = ConfigsT $ ReaderT $ \configs ->
99+ performEvent $ runConfigsT configs <$> e
100+ performEvent_ e = ConfigsT $ ReaderT $ \configs ->
101+ performEvent_ $ runConfigsT configs <$> e
102+
103+instance Adjustable t m => Adjustable t (ConfigsT m) where
104+ runWithReplace a e = ConfigsT $ runWithReplace (unConfigsT a) (unConfigsT <$> e)
105+ traverseDMapWithKeyWithAdjust f m e = ConfigsT $ traverseDMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
106+ traverseIntMapWithKeyWithAdjust f m e = ConfigsT $ traverseIntMapWithKeyWithAdjust (\k v -> unConfigsT $ f k v) m e
107+ traverseDMapWithKeyWithAdjustWithMove f m e = ConfigsT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> unConfigsT $ f k v) m e
108+
109+instance Prerender js t m => Prerender js t (ConfigsT m) where
110+ type Client (ConfigsT m) = ConfigsT (Client m)
111+ prerender server client = ConfigsT $ ReaderT $ \configs ->
112+ prerender (runConfigsT configs server) (runConfigsT configs client)
113+
114+instance PrimMonad m => PrimMonad (ConfigsT m) where
115+ type PrimState (ConfigsT m) = PrimState m
116+ primitive = lift . primitive
117+
118+runConfigsT
119+ :: Map Text ByteString
120+ -> ConfigsT m a
121+ -> m a
122+runConfigsT cs child = runReaderT (unConfigsT child) cs
123+
124+mapConfigsT
125+ :: (forall x. m x -> n x)
126+ -> ConfigsT m a
127+ -> ConfigsT n a
128+mapConfigsT f (ConfigsT x) = ConfigsT $ mapReaderT f x
lib/executable-config/lookup/src/Obelisk/Configs/Internal/Directory.hsnew file+24
@@ -0,0 +1,24 @@
1+{-# LANGUAGE LambdaCase #-}
2+{-# LANGUAGE OverloadedStrings #-}
3+module Obelisk.Configs.Internal.Directory where
4+
5+import Control.Monad
6+import Data.ByteString (ByteString)
7+import qualified Data.ByteString as BS
8+import Data.Map (Map)
9+import qualified Data.Map as Map
10+import Data.Text (Text)
11+import qualified Data.Text as T
12+import System.Directory
13+import System.FilePath.Posix
14+
15+getConfigsFromDirectory :: FilePath -> IO (Map Text ByteString)
16+getConfigsFromDirectory base = doesDirectoryExist base >>= \case
17+ True -> do
18+ ps <- listDirectory base
19+ fmap mconcat $ forM ps $ \p -> do
20+ subdirConfigs <- getConfigsFromDirectory $ base </> p
21+ pure $ Map.mapKeys (T.pack . (p </>) . T.unpack) subdirConfigs
22+ False -> doesFileExist base >>= \case
23+ True -> Map.singleton "" <$> BS.readFile base
24+ False -> pure mempty
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hsdeleted−13
@@ -1,13 +0,0 @@
1−module Obelisk.ExecutableConfig (get) where
2−
3−import Control.Exception
4−import Data.Text (Text)
5−import Data.Text as T
6−import Data.Text.IO as T
7−import System.FilePath.Posix ((</>))
8−import System.IO.Error
9−
10−get :: Text -> IO (Maybe Text)
11−get path = do
12− let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/frontend/obelisk-frontend.cabal+2
@@ -7,6 +7,7 @@ library
77 hs-source-dirs: src
88 build-depends: base,
99 bytestring,
10+ containers,
1011 cookie,
1112 dependent-sum,
1213 ghcjs-dom,
@@ -14,6 +15,7 @@ library
1415 lens,
1516 mtl,
1617 obelisk-executable-config-inject,
18+ obelisk-executable-config-lookup,
1719 obelisk-route,
1820 primitive,
1921 ref-tf,
lib/frontend/src/Obelisk/Frontend.hs+150−25
@@ -1,3 +1,4 @@
1+{-# LANGUAGE CPP #-}
12{-# LANGUAGE ConstraintKinds #-}
23{-# LANGUAGE FlexibleContexts #-}
34{-# LANGUAGE FlexibleInstances #-}
@@ -14,7 +15,10 @@ module Obelisk.Frontend
1415 ( ObeliskWidget
1516 , Frontend (..)
1617 , runFrontend
18+ , runFrontendWithConfigsAndCurrentRoute
1719 , renderFrontendHtml
20+ , removeHTMLConfigs
21+ , FrontendMode (..)
1822 , module Obelisk.Frontend.Cookie
1923 ) where
2024
@@ -27,18 +31,35 @@ import Control.Monad.IO.Class
2731import Control.Monad.Primitive
2832import Control.Monad.Reader
2933import Control.Monad.Ref
34+import Control.Monad.Trans.Maybe (MaybeT(..), runMaybeT)
3035import Data.ByteString (ByteString)
36+import Data.Foldable (for_)
3137import Data.Functor.Sum
38+import Data.Map (Map)
39+import Data.Maybe (catMaybes)
3240import Data.Monoid ((<>))
3341import Data.Text (Text)
34−import Language.Javascript.JSaddle (JSM)
42+import qualified GHCJS.DOM as DOM
43+import qualified GHCJS.DOM.Types as DOM
44+import qualified GHCJS.DOM.History as DOM
45+import qualified GHCJS.DOM.Window as DOM
46+import Language.Javascript.JSaddle (JSM, jsNull)
47+import GHCJS.DOM (currentDocument)
48+import GHCJS.DOM.Document (getHead)
49+import GHCJS.DOM.Node (removeChild_)
50+import GHCJS.DOM.NodeList (item, getLength)
51+import GHCJS.DOM.ParentNode (querySelectorAll)
3552import Obelisk.Frontend.Cookie
3653import Obelisk.Route.Frontend
3754import Reflex.Dom.Core
3855import Reflex.Host.Class
56+import Obelisk.Configs
3957import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
58+import qualified Obelisk.ExecutableConfig.Lookup as Lookup
4059import Web.Cookie
4160
61+import Debug.Trace
62+
4263makePrisms ''Sum
4364
4465type ObeliskWidget js t route m =
@@ -60,9 +81,8 @@ type ObeliskWidget js t route m =
6081 , Prerender js t m
6182 , PrebuildAgnostic t route m
6283 , PrebuildAgnostic t route (Client m)
84+ , HasConfigs m
6385 , HasCookies m
64− -- TODO Remove these. Probably requires a new class to allow executable-configs to work without being inside a `prerender`
65− , MonadIO m
6686 , MonadIO (Performable m)
6787 )
6888
@@ -70,6 +90,8 @@ type PrebuildAgnostic t route m =
7090 ( SetRoute t route m
7191 , RouteToUrl route m
7292 , MonadFix m
93+ , HasConfigs m
94+ , HasConfigs (Performable m)
7395 )
7496
7597data Frontend route = Frontend
@@ -77,46 +99,149 @@ data Frontend route = Frontend
7799 , _frontend_body :: !(forall js t m. ObeliskWidget js t route m => RoutedT t route m ())
78100 }
79101
102+baseTag :: forall route js t m. ObeliskWidget js t route m => RoutedT t route m ()
103+baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
104+
105+removeHTMLConfigs :: JSM ()
106+removeHTMLConfigs = void $ runMaybeT $ do
107+ doc <- MaybeT currentDocument
108+ hd <- MaybeT $ getHead doc
109+ es <- collToList =<< querySelectorAll hd ("[data-obelisk-executable-config-inject-key]" :: Text)
110+ for_ es $ removeChild_ hd
111+ where
112+ collToList es = do
113+ len <- getLength es
114+ lst <- traverse (item es) $ take (fromIntegral len) $ [0..] -- fun with unsigned types ...
115+ pure $ catMaybes lst
116+
117+setInitialRoute :: Bool -> JSM ()
118+setInitialRoute useHash = do
119+ traceM "setInitialRoute"
120+ window <- DOM.currentWindowUnchecked
121+ initialLocation <- DOM.getLocation window
122+ initialUri <- getLocationUri initialLocation
123+ history <- DOM.getHistory window
124+ DOM.replaceState history jsNull ("" :: Text) $ Just $
125+ show $ setAdaptedUriPath useHash "/" initialUri
126+
127+data FrontendMode = FrontendMode
128+ { _frontendMode_hydrate :: Bool
129+ -- ^ There is already a rendering of the DOM in place; hydrate it rather
130+ -- than building new DOM
131+ , _frontendMode_adjustRoute :: Bool
132+ -- ^ The page can't use regular routes, so encode routes into the hash
133+ -- instead
134+ }
135+
136+-- | Run the frontend, setting the initial route to "/" on platforms where no
137+-- route exists ambiently in the context (e.g. anything but web).
138+-- Selects FrontendMode based on platform; this doesn't work for jsaddle-warp
80139runFrontend
81140 :: forall backendRoute route
82− . Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
141+ . Encoder Identity Identity (R (FullRoute backendRoute route)) PageName
83142 -> Frontend (R route)
84143 -> JSM ()
85144runFrontend validFullEncoder frontend = do
86− let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
145+ let mode = FrontendMode
146+ { _frontendMode_hydrate =
147+#ifdef ghcjs_HOST_OS
148+ True
149+#else
150+ False
151+#endif
152+ , _frontendMode_adjustRoute =
153+#ifdef ghcjs_HOST_OS
154+ False
155+#else
156+ True
157+#endif
158+ }
159+ configs <- liftIO Lookup.getConfigs
160+ when (_frontendMode_hydrate mode) removeHTMLConfigs
161+ -- There's no fundamental reason that adjustRoute needs to control setting the
162+ -- initial route and *also* the useHash parameter; that's why these are
163+ -- separate here. However, currently, they are always the same.
164+ when (_frontendMode_adjustRoute mode) $ do
165+ setInitialRoute $ _frontendMode_adjustRoute mode
166+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
167+
168+runFrontendWithConfigsAndCurrentRoute
169+ :: forall backendRoute frontendRoute
170+ . FrontendMode
171+ -> Map Text ByteString
172+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
173+ -> Frontend (R frontendRoute)
174+ -> JSM ()
175+runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend = do
176+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _FullRoute_Frontend . _ObeliskRoute_App))
87177 errorLeft = \case
88178 Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
89179 Right x -> Identity x
90− runHydrationWidgetWithHeadAndBody (pure ()) $ \appendHead appendBody -> do
91− rec switchover <- runRouteViewT ve switchover $ do
92− (switchover'', fire) <- newTriggerEvent
93− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendHead)) $ do
94− _frontend_head frontend
95− mapRoutedT (mapSetRouteT (mapRouteToUrlT appendBody)) $ do
96− _frontend_body frontend
97− switchover' <- lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
98− performEvent_ $ liftIO (fire ()) <$ switchover'
99− pure switchover''
100− pure ()
180+ w :: ( RawDocument (DomBuilderSpace (HydrationDomBuilderT s DomTimeline m)) ~ DOM.Document
181+ , Ref (Performable m) ~ Ref IO
182+ , Ref m ~ Ref IO
183+ , DomBuilder DomTimeline (HydrationDomBuilderT s DomTimeline m)
184+ , MonadHold DomTimeline m
185+ , MonadRef m
186+ , MonadRef (Performable m)
187+ , MonadReflexCreateTrigger DomTimeline m
188+ , PerformEvent DomTimeline m
189+ , PostBuild DomTimeline m
190+ , PrimMonad m
191+ , MonadSample DomTimeline (Performable m)
192+ , DOM.MonadJSM m
193+ , Monad (Performable (Client (HydrationDomBuilderT s DomTimeline m)))
194+ , MonadFix (Client (HydrationDomBuilderT s DomTimeline m))
195+ , MonadFix (Performable m)
196+ , MonadFix m
197+ , Prerender js DomTimeline (HydrationDomBuilderT s DomTimeline m)
198+ , MonadIO (Performable m)
199+ )
200+ => (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
201+ -> (forall c. HydrationDomBuilderT s DomTimeline m c -> FloatingWidget () c)
202+ -> FloatingWidget () ()
203+ w appendHead appendBody = do
204+ rec switchover <- runRouteViewT ve switchover (_frontendMode_adjustRoute mode) $ do
205+ (switchover'', fire) <- newTriggerEvent
206+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendHead . runConfigsT configs))) $ do
207+ -- The order here is important - baseTag has to be before headWidget!
208+ baseTag
209+ _frontend_head frontend
210+ mapRoutedT (mapSetRouteT (mapRouteToUrlT (appendBody . runConfigsT configs))) $ do
211+ _frontend_body frontend
212+ switchover' <- case _frontendMode_hydrate mode of
213+ True -> lift $ lift $ lift $ lift $ HydrationDomBuilderT $ asks _hydrationDomBuilderEnv_switchover
214+ False -> getPostBuild
215+ performEvent_ $ liftIO (fire ()) <$ switchover'
216+ pure switchover''
217+ pure ()
218+ if _frontendMode_hydrate mode
219+ then runHydrationWidgetWithHeadAndBody (pure ()) w
220+ else runImmediateWidgetWithHeadAndBody w
101221
102222renderFrontendHtml
103223 :: ( t ~ DomTimeline
104224 , MonadIO m
105− , widget ~ RoutedT t r (SetRouteT t r' (RouteToUrlT r' (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost))))))
225+ , widget ~ RoutedT t r (SetRouteT t r (RouteToUrlT r (ConfigsT (CookiesT (PostBuildT t (StaticDomBuilderT t (PerformEventT t DomHost)))))))
106226 )
107− => Cookies
108− -> (r' -> Text)
227+ => Map Text ByteString
228+ -> Cookies
229+ -> (r -> Text)
109230 -> r
231+ -> Frontend r
110232 -> widget ()
111233 -> widget ()
112234 -> m ByteString
113−renderFrontendHtml cookies urlEnc route headWidget bodyWidget = do
235+renderFrontendHtml configs cookies urlEnc route frontend headExtra bodyExtra = do
114236 --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
115− html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
237+ html <- fmap snd $ liftIO $ renderStatic $ fmap fst $ runCookiesT cookies $ runConfigsT configs $ flip runRouteToUrlT urlEnc $ runSetRouteT $ flip runRoutedT (pure route) $
116238 el "html" $ do
117239 el "head" $ do
118− let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
119− -- The order here is important - baseTag has to be before headWidget!
120− baseTag >> injectExecutableConfigs >> headWidget
121− el "body" bodyWidget
240+ baseTag
241+ injectExecutableConfigs configs
242+ _frontend_head frontend
243+ headExtra
244+ el "body" $ do
245+ _frontend_body frontend
246+ bodyExtra
122247 return $ "<!DOCTYPE html>" <> html
lib/frontend/src/Obelisk/Frontend/Cookie.hs+3
@@ -26,6 +26,7 @@ import GHCJS.DOM.Document (getCookie, Document)
2626import GHCJS.DOM.Types (MonadJSM)
2727import Web.Cookie
2828
29+import Obelisk.Configs
2930import Obelisk.Route.Frontend
3031
3132class Monad m => HasCookies m where
@@ -45,6 +46,8 @@ instance HasCookies m => HasCookies (SetRouteT t r m)
4546instance HasCookies m => HasCookies (StaticDomBuilderT t m)
4647instance HasCookies m => HasCookies (TriggerEventT t m)
4748instance HasCookies m => HasCookies (RoutedT t r m)
49+instance HasCookies m => HasCookies (ConfigsT m)
50+instance HasConfigs m => HasConfigs (CookiesT m)
4851
4952newtype CookiesT m a = CookiesT { unCookiesT :: ReaderT Cookies m a }
5053 deriving
lib/route/obelisk-route.cabal+7−2
@@ -6,12 +6,14 @@ build-type: Simple
66library
77 hs-source-dirs: src
88 build-depends: base,
9+ aeson,
10+ bytestring,
911 categories,
1012 constraints,
13+ constraints-extras,
1114 containers,
1215 dependent-map,
1316 dependent-sum,
14− dependent-sum-universe-orphans,
1517 dependent-sum-template,
1618 either,
1719 ghcjs-dom,
@@ -21,15 +23,18 @@ library
2123 monad-control,
2224 mtl,
2325 network-uri,
26+ obelisk-executable-config-lookup,
2427 primitive,
2528 ref-tf,
2629 reflex,
2730 reflex-dom-core,
2831 template-haskell,
32+ tabulation,
2933 text,
34+ th-extras,
3035 transformers,
3136 universe,
32− universe-template
37+ universe-dependent-sum
3338 exposed-modules: Obelisk.Route
3439 Obelisk.Route.TH
3540 Obelisk.Route.Frontend
lib/route/src/Obelisk/Route.hs+298−53
@@ -12,7 +12,9 @@
1212{-# LANGUAGE PatternSynonyms #-}
1313{-# LANGUAGE RankNTypes #-}
1414{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE StandaloneDeriving #-}
1516{-# LANGUAGE TemplateHaskell #-}
17+{-# LANGUAGE TypeApplications #-}
1618{-# LANGUAGE TypeFamilies #-}
1719{-# LANGUAGE TypeOperators #-}
1820{-# LANGUAGE ViewPatterns #-}
@@ -61,10 +63,15 @@ module Obelisk.Route
6163 , shadowEncoder
6264 , prismEncoder
6365 , rPrism
66+ , _R
6467 , obeliskRouteEncoder
6568 , obeliskRouteSegment
6669 , pageNameEncoder
6770 , handleEncoder
71+ , FullRoute (..)
72+ , _FullRoute_Frontend
73+ , _FullRoute_Backend
74+ , mkFullRouteEncoder
6875 , ObeliskRoute (..)
6976 , _ObeliskRoute_App
7077 , _ObeliskRoute_Resource
@@ -85,6 +92,13 @@ module Obelisk.Route
8592 , readShowEncoder
8693 , integralEncoder
8794 , pathSegmentEncoder
95+ , queryOnlyEncoder
96+ , Decoder(..)
97+ , dmapEncoder
98+ , fieldMapEncoder
99+ , pathFieldEncoder
100+ , jsonEncoder
101+ , byteStringsToPageName
88102 ) where
89103
90104import Prelude hiding ((.), id)
@@ -95,8 +109,32 @@ import qualified Control.Categorical.Functor as Cat
95109import Control.Categorical.Bifunctor
96110import Control.Category.Associative
97111import Control.Category.Monoidal
98−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+ )
99132import 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
137+import qualified Data.ByteString.Lazy as BSL
100138import Data.Dependent.Sum (DSum (..))
101139import Data.Dependent.Map (DMap)
102140import qualified Data.Dependent.Map as DMap
@@ -113,17 +151,19 @@ import Data.Maybe
113151import Data.Monoid ((<>))
114152import Data.Set (Set)
115153import qualified Data.Set as Set
116−import Data.Some (Some)
117−import qualified Data.Some as Some
118−import Data.Some.Universe.Orphans ()
154+import Data.Some (Some(Some))
119155import Data.Text (Text)
120156import qualified Data.Text as T
121−import Data.Text.Encoding
157+import qualified Data.Text.Encoding as T
122158import Data.Universe
159+import Data.Universe.Some
123160import Network.HTTP.Types.URI
124161import qualified Numeric.Lens
125162import Obelisk.Route.TH
126163import Text.Read (readMaybe)
164+import Data.Tabulation
165+import qualified Data.Aeson as Aeson
166+import Data.Aeson (FromJSON, ToJSON)
127167
128168-- Design goals:
129169-- No start-up time on the frontend (not yet met)
@@ -149,13 +189,13 @@ import Text.Read (readMaybe)
149189
150190type R f = DSum f Identity --TODO: Better name
151191
152−--TODO: COMPLETE pragma
192+{-# COMPLETE (:/) #-}
153193infixr 5 :/
154194pattern (:/) :: f a -> a -> R f
155195pattern a :/ b = a :=> Identity b
156196
157197mapSome :: (forall a. f a -> g a) -> Some f -> Some g
158−mapSome f (Some.This a) = Some.This $ f a
198+mapSome f (Some a) = Some $ f a
159199
160200hoistR :: (forall x. f x -> g x) -> R f -> R g
161201hoistR f (x :=> Identity y) = f x :/ y
@@ -243,6 +283,10 @@ instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (E
243283 , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
244284 }
245285
286+instance (Monad parse, Applicative check) => Braided (Encoder check parse) (,) where
287+ braid = isoEncoder (iso swap swap)
288+
289+
246290instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
247291 first f = bimap f id
248292instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
@@ -256,6 +300,34 @@ instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (Enco
256300 , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
257301 }
258302
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+
259331instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
260332 fmap e = Encoder $ do
261333 ve <- unEncoder e
@@ -343,14 +415,14 @@ maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
343415justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
344416justEncoder = prismEncoder _Just
345417
346−-- |
418+-- | Encode () to 'Nothing'.
347419nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
348420nothingEncoder = prismEncoder _Nothing
349421
350422someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
351423someConstEncoder = unsafeMkEncoder $ EncoderImpl
352− { _encoderImpl_encode = \(Some.This (Const a)) -> a
353− , _encoderImpl_decode = pure . Some.This . Const
424+ { _encoderImpl_encode = \(Some (Const a)) -> a
425+ , _encoderImpl_decode = pure . Some . Const
354426 }
355427
356428-- | WARNING: This is only safe if the Show and Read instances for 'a' are
@@ -378,7 +450,7 @@ checkEnum1EncoderFunc
378450 -> check (EncoderFunc check' parse p r)
379451checkEnum1EncoderFunc f = do
380452 (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
381− traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
453+ traverse (\(Some p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
382454 pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
383455 DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
384456
@@ -433,11 +505,11 @@ chainEncoder cons this rest = Encoder $ do
433505 pure $ EncoderImpl
434506 { _encoderImpl_decode = \v -> do
435507 (here, following) <- _encoderImpl_decode consValid v
436− Some.This r <- _encoderImpl_decode thisValid here
508+ Some r <- _encoderImpl_decode thisValid here
437509 (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
438510 , _encoderImpl_encode = \(r :/ s) ->
439511 _encoderImpl_encode consValid
440− ( _encoderImpl_encode thisValid $ Some.This r
512+ ( _encoderImpl_encode thisValid $ Some r
441513 , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
442514 }
443515
@@ -504,7 +576,7 @@ enum1Encoder
504576 , Show r
505577 )
506578 => (forall a. p a -> r) -> Encoder check parse (Some p) r
507−enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
579+enum1Encoder f = enumEncoder $ \(Some p) -> f p
508580
509581-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
510582-- have a large number of values - it will use a lot of memory.
@@ -547,13 +619,10 @@ pathOnlyEncoderIgnoringQuery = unsafeMkEncoder $ EncoderImpl
547619 }
548620
549621pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
550−pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
551− { _encoderImpl_decode = \(path, query) ->
552− if query == mempty
553− then pure path
554− else throwError "pathOnlyEncoderImpl: query was provided"
555− , _encoderImpl_encode = \path -> (path, mempty)
556− }
622+pathOnlyEncoder = second (unitEncoder mempty) . coidr
623+
624+queryOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Map Text (Maybe Text)) PageName
625+queryOnlyEncoder = first (unitEncoder []) . coidl
557626
558627singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
559628singletonListEncoder = unsafeMkEncoder $ EncoderImpl
@@ -596,10 +665,10 @@ queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
596665 in (urlDecodeText True k, urlDecodeText True <$> mv)
597666
598667urlEncodeText :: Bool -> Text -> Text
599−urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
668+urlEncodeText q = T.decodeUtf8 . urlEncode q . T.encodeUtf8
600669
601670urlDecodeText :: Bool -> Text -> Text
602−urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
671+urlDecodeText q = T.decodeUtf8 . urlDecode q . T.encodeUtf8
603672
604673listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
605674listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
@@ -657,10 +726,47 @@ joinPairTextEncoder = Encoder . \case
657726 _ -> return (kt, T.drop (T.length separator) vt)
658727 }
659728
660−--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
661−-- Or, more likely, the user can compose it themselves
662−rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
663−rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
729+-- This slight generalization of 'rPrism' happens to be enough to write all our
730+-- prism combinators so far.
731+dSumPrism
732+ :: forall f f' g
733+ . (forall a. Prism' (f a) (f' a))
734+ -> Prism' (DSum f g) (DSum f' g)
735+dSumPrism p = prism'
736+ (\(f' :=> x) -> f' ^. re p :=> x)
737+ (\(f :=> x) -> (:=> x) <$> (f ^? p))
738+
739+-- already in obelisk
740+rPrism
741+ :: forall f f'
742+ . (forall a. Prism' (f a) (f' a))
743+ -> Prism' (R f) (R f')
744+rPrism p = dSumPrism p
745+
746+dSumPrism'
747+ :: forall f g a
748+ . (forall b. Prism' (f b) (a :~: b))
749+ -> Prism' (DSum f g) (g a)
750+dSumPrism' p = dSumPrism p . iso (\(Refl :=> b) -> b) (Refl :=>)
751+
752+dSumGEqPrism
753+ :: GEq f
754+ => f a
755+ -> Prism' (DSum f g) (g a)
756+dSumGEqPrism variant = dSumPrism' $ prism' (\Refl -> variant) (\x -> geq variant x)
757+
758+-- | Given a 'tag :: f a', make a prism for 'R f'. This generalizes the usual
759+-- prisms for a sum type (the ones that 'mkPrisms' would make), just as 'R'
760+-- generalized a usual sum type.
761+--
762+-- [This is given the '_R' name of the "cannonical" prism not because it is the
763+-- most general, but because it seems the most useful for routes, and 'R' itself
764+-- trades generality for route-specificity.]
765+_R
766+ :: GEq f
767+ => f a
768+ -> Prism' (R f) a
769+_R variant = dSumGEqPrism variant . iso runIdentity Identity
664770
665771-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
666772-- if the prism doesn't match.
@@ -704,6 +810,45 @@ handleEncoder recover e = Encoder $ do
704810-- Actual obelisk route info
705811--------------------------------------------------------------------------------
706812
813+-- | The typical full route type comprising all of an Obelisk application's routes.
814+-- Parameterised by the top level GADTs that define backend and frontend routes, respectively.
815+data FullRoute :: (* -> *) -> (* -> *) -> * -> * where
816+ FullRoute_Backend :: br a -> FullRoute br fr a
817+ FullRoute_Frontend :: ObeliskRoute fr a -> FullRoute br fr a
818+
819+instance (GShow br, GShow fr) => GShow (FullRoute br fr) where
820+ gshowsPrec p = \case
821+ FullRoute_Backend x -> showParen (p > 10) (showString "FullRoute_Backend " . gshowsPrec 11 x)
822+ FullRoute_Frontend x -> showParen (p > 10) (showString "FullRoute_Frontend " . gshowsPrec 11 x)
823+
824+instance (GEq br, GEq fr) => GEq (FullRoute br fr) where
825+ geq (FullRoute_Backend x) (FullRoute_Backend y) = geq x y
826+ geq (FullRoute_Frontend x) (FullRoute_Frontend y) = geq x y
827+ geq _ _ = Nothing
828+
829+instance (GCompare br, GCompare fr) => GCompare (FullRoute br fr) where
830+ gcompare (FullRoute_Backend _) (FullRoute_Frontend _) = GLT
831+ gcompare (FullRoute_Frontend _) (FullRoute_Backend _) = GGT
832+ gcompare (FullRoute_Backend x) (FullRoute_Backend y) = gcompare x y
833+ gcompare (FullRoute_Frontend x) (FullRoute_Frontend y) = gcompare x y
834+
835+instance (UniverseSome br, UniverseSome fr) => UniverseSome (FullRoute br fr) where
836+ universeSome = [Some (FullRoute_Backend x) | Some x <- universeSome]
837+ ++ [Some (FullRoute_Frontend x) | Some x <- universeSome]
838+
839+-- | Build the typical top level application route encoder from a route for handling 404's,
840+-- and segment encoders for backend and frontend routes.
841+mkFullRouteEncoder
842+ :: (GCompare br, GCompare fr, GShow br, GShow fr, UniverseSome br, UniverseSome fr)
843+ => R (FullRoute br fr) -- ^ 404 handler
844+ -> (forall a. br a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single backend route segment
845+ -> (forall a. fr a -> SegmentResult (Either Text) (Either Text) a) -- ^ How to encode a single frontend route segment
846+ -> Encoder (Either Text) Identity (R (FullRoute br fr)) PageName
847+mkFullRouteEncoder missing backendSegment frontendSegment = handleEncoder (const missing) $
848+ pathComponentEncoder $ \case
849+ FullRoute_Backend backendRoute -> backendSegment backendRoute
850+ FullRoute_Frontend obeliskRoute -> obeliskRouteSegment obeliskRoute frontendSegment
851+
707852-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
708853-- frontend.
709854data ObeliskRoute :: (* -> *) -> * -> * where
@@ -711,9 +856,11 @@ data ObeliskRoute :: (* -> *) -> * -> * where
711856 ObeliskRoute_App :: f a -> ObeliskRoute f a
712857 ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
713858
714−instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
715− universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
716− ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
859+instance UniverseSome f => UniverseSome (ObeliskRoute f) where
860+ universeSome = concat
861+ [ (\(Some x) -> Some (ObeliskRoute_App x)) <$> universe
862+ , (\(Some x) -> Some (ObeliskRoute_Resource x)) <$> universe
863+ ]
717864
718865instance GEq f => GEq (ObeliskRoute f) where
719866 geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
@@ -801,12 +948,12 @@ indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
801948
802949someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
803950someSumEncoder = Encoder $ pure $ EncoderImpl
804− { _encoderImpl_encode = \(Some.This t) -> case t of
805− InL l -> Left $ Some.This l
806− InR r -> Right $ Some.This r
951+ { _encoderImpl_encode = \(Some t) -> case t of
952+ InL l -> Left $ Some l
953+ InR r -> Right $ Some r
807954 , _encoderImpl_decode = pure . \case
808− Left (Some.This l) -> Some.This (InL l)
809− Right (Some.This r) -> Some.This (InR r)
955+ Left (Some l) -> Some (InL l)
956+ Right (Some r) -> Some (InR r)
810957 }
811958
812959data Void1 :: * -> * where {}
@@ -817,7 +964,7 @@ instance Universe (Some Void1) where
817964void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
818965void1Encoder = Encoder $ pure $ EncoderImpl
819966 { _encoderImpl_encode = \case
820− Some.This f -> case f of {}
967+ Some f -> case f of {}
821968 , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
822969 }
823970
@@ -831,7 +978,7 @@ concat <$> mapM deriveRouteComponent
831978 ]
832979
833980makePrisms ''ObeliskRoute
834−
981+makePrisms ''FullRoute
835982deriveGEq ''Void1
836983deriveGCompare ''Void1
837984
@@ -839,44 +986,142 @@ deriveGCompare ''Void1
839986-- and query string). See 'checkEncoder' for how to produce a checked encoder.
840987renderBackendRoute
841988 :: forall br a.
842− Encoder Identity Identity (R (Sum br a)) PageName
989+ Encoder Identity Identity (R (FullRoute br a)) PageName
843990 -> R br
844991 -> Text
845−renderBackendRoute enc = renderObeliskRoute enc . hoistR InL
992+renderBackendRoute enc = renderObeliskRoute enc . hoistR FullRoute_Backend
846993
847994-- | Renders a frontend route with the supplied checked encoder
848995renderFrontendRoute
849996 :: forall a fr.
850− Encoder Identity Identity (R (Sum a (ObeliskRoute fr))) PageName
997+ Encoder Identity Identity (R (FullRoute a fr)) PageName
851998 -> R fr
852999 -> Text
853−renderFrontendRoute enc = renderObeliskRoute enc . hoistR (InR . ObeliskRoute_App)
1000+renderFrontendRoute enc = renderObeliskRoute enc . hoistR (FullRoute_Frontend . ObeliskRoute_App)
8541001
8551002-- | Renders a route of the form typically found in an Obelisk project
8561003renderObeliskRoute
8571004 :: forall a b.
858− Encoder Identity Identity (R (Sum a b)) PageName
859− -> R (Sum a b)
1005+ Encoder Identity Identity (R (FullRoute a b)) PageName
1006+ -> R (FullRoute a b)
8601007 -> Text
8611008renderObeliskRoute e r =
862− let enc :: Encoder Identity (Either Text) (R (Sum a b)) PathQuery
1009+ let enc :: Encoder Identity (Either Text) (R (FullRoute a b)) PathQuery
8631010 enc = (pageNameEncoder . hoistParse (pure . runIdentity) e)
8641011 in (T.pack . uncurry (<>)) $ encode enc r
8651012
8661013readShowEncoder :: (MonadError Text parse, Read a, Show a, Applicative check) => Encoder check parse a PageName
8671014readShowEncoder = singlePathSegmentEncoder . unsafeTshowEncoder
8681015
869−
8701016integralEncoder :: (MonadError Text parse, Applicative check, Integral a) => Encoder check parse a Integer
8711017integralEncoder = prismEncoder (Numeric.Lens.integral)
8721018
873−pathSegmentEncoder :: (MonadError Text parse, Applicative check) =>
874− Encoder check parse (Text, PageName) PageName
875−pathSegmentEncoder = unsafeMkEncoder EncoderImpl
876− { _encoderImpl_encode = \(x, (y, z)) -> (x:y, z)
877− , _encoderImpl_decode = \(xss, y) -> case xss of
878− [] -> throwError "not enough path segments"
879− (x:xs) -> pure (x, (xs, y))
880− }
1019+pathSegmentEncoder :: (MonadError Text parse, Applicative check, Cons as as a a) =>
1020+ Encoder check parse (a, (as, b)) (as, b)
1021+pathSegmentEncoder = first (prismEncoder _Cons) . disassociate
1022+
1023+newtype Decoder check parse b a = Decoder { toEncoder :: Encoder check parse a b }
1024+
1025+dmapEncoder :: forall check parse k k' v.
1026+ ( Monad check
1027+ , MonadError Text parse
1028+ , Universe (Some k')
1029+ , Ord k
1030+ , GCompare k'
1031+ , GShow k'
1032+ )
1033+ => Encoder check parse (Some k') k
1034+ -> (forall v'. k' v' -> Encoder check parse v' v)
1035+ -> Encoder check parse (DMap k' Identity) (Map k v)
1036+dmapEncoder keyEncoder' valueEncoderFor = unsafeEncoder $ do
1037+ keyEncoder :: Encoder Identity parse (Some k') k <- checkEncoder keyEncoder'
1038+ valueDecoders :: DMap k' (Decoder Identity parse v) <- fmap DMap.fromList . forM universe $ \(Some (k' :: k' t)) -> do
1039+ ve :: Encoder Identity parse t v <- checkEncoder (valueEncoderFor k')
1040+ return $ (k' :: k' t) :=> (Decoder ve :: Decoder Identity parse v t)
1041+ let keyError k = "dmapEncoder: key `" <> k <> "' was missing from the Universe instance for its type."
1042+ return $ EncoderImpl
1043+ { _encoderImpl_encode = \dm -> Map.fromList $ do
1044+ ((k' :: k' t) :=> Identity v') <- DMap.toList dm
1045+ return ( encode keyEncoder (Some k')
1046+ , encode (toEncoder (DMap.findWithDefault (error . keyError $ gshow k') k' valueDecoders)) v'
1047+ )
1048+ , _encoderImpl_decode = \m -> fmap DMap.fromList . forM (Map.toList m) $ \(k,v) -> do
1049+ Some (k' :: k' t) <- tryDecode keyEncoder k
1050+ case DMap.lookup k' valueDecoders of
1051+ Nothing -> throwError . T.pack . keyError $ gshow k'
1052+ Just (Decoder e) -> do
1053+ v' <- tryDecode e v
1054+ return (k' :=> Identity v')
1055+ }
1056+
1057+fieldMapEncoder :: forall check parse r.
1058+ ( Applicative check
1059+ , MonadError Text parse
1060+ , HasFields r
1061+ , Universe (Some (Field r))
1062+ , GShow (Field r)
1063+ , GCompare (Field r)
1064+ )
1065+ => Encoder check parse r (DMap (Field r) Identity)
1066+fieldMapEncoder = unsafeEncoder $ do
1067+ pure $ EncoderImpl
1068+ { _encoderImpl_encode = \r -> DMap.fromList [ f :=> Identity (indexField r f) | Some f <- universe ]
1069+ , _encoderImpl_decode = \dm -> tabulateFieldsA $ \f -> do
1070+ case DMap.lookup f dm of
1071+ Nothing -> throwError $ "fieldMapEncoder: Couldn't find key for `" <> T.pack (gshow f) <> "' in DMap."
1072+ Just (Identity v) -> return v
1073+ }
1074+
1075+-- this is in base 4.12 (GHC 8.6);
1076+newtype Ap f a = Ap {getAp :: f a}
1077+
1078+instance (Applicative f, Semigroup a) => Semigroup (Ap f a) where
1079+ Ap x <> Ap y = Ap (liftA2 (<>) x y)
1080+
1081+instance (Applicative f, Monoid a) => Monoid (Ap f a) where
1082+ mappend = (<>)
1083+ mempty = Ap (pure mempty)
1084+
1085+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]
1086+pathFieldEncoder fieldEncoder = unsafeEncoder $ do
1087+ 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)
1088+ let fieldEncoderPure :: forall x. Field a x -> Encoder Identity parse x p
1089+ fieldEncoderPure f = toEncoder (DMap.findWithDefault (error "bad") f fieldEncoderPureMap)
1090+ pure $ EncoderImpl
1091+ { _encoderImpl_encode = \(x, rest) -> execWriter $ do
1092+ _ <- traverseWithField (\f x_i -> tell (pure $ encode (fieldEncoderPure f) x_i) *> pure x_i) x
1093+ tell rest
1094+ , _encoderImpl_decode = State.runStateT $ tabulateFieldsA $ \f -> State.get >>= \case
1095+ [] -> throwError $ T.pack "not enough path components"
1096+ p:ps -> do
1097+ State.put ps
1098+ lift $ tryDecode (fieldEncoderPure f) p
1099+ }
1100+
1101+-- | Use ToJSON/FromJSON to encode to Text. The correctness of this encoder is dependent on the encoding being injective and round-tripping correctly.
1102+jsonEncoder :: forall check parse r.
1103+ ( ToJSON r
1104+ , FromJSON r
1105+ , Applicative check
1106+ , MonadError Text parse
1107+ )
1108+ => Encoder check parse r Text
1109+jsonEncoder = unsafeEncoder $ do
1110+ pure $ EncoderImpl
1111+ { _encoderImpl_encode = \r -> T.decodeUtf8 . BSL.toStrict $ Aeson.encode r
1112+ , _encoderImpl_decode = \t -> case Aeson.eitherDecodeStrict $ T.encodeUtf8 t of
1113+ Left err -> throwError ("jsonEncoder: " <> T.pack err)
1114+ Right x -> return x
1115+ }
1116+
1117+-- Useful for app server integration.
1118+-- p must not start with slashes
1119+byteStringsToPageName :: BS.ByteString -> BS.ByteString -> PageName
1120+byteStringsToPageName p q =
1121+ let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
1122+ pageNameEncoder' = bimap
1123+ (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
1124+ (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
1125+ in decode pageNameEncoder' (T.unpack (T.decodeUtf8 p), T.unpack (T.decodeUtf8 q))
8811126
8821127--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hs+51−20
@@ -51,6 +51,9 @@ module Obelisk.Route.Frontend
5151 , runRouteToUrlT
5252 , mapRouteToUrlT
5353 , routeLink
54+ , dynRouteLink
55+ , adaptedUriPath
56+ , setAdaptedUriPath
5457 ) where
5558
5659import Prelude hiding ((.), id)
@@ -88,13 +91,13 @@ import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can
8891import Reflex.Dom.Core
8992import qualified GHCJS.DOM.Types as DOM
9093import Network.URI
91−#if defined(ios_HOST_OS)
9294import Data.Maybe (fromMaybe)
9395import qualified Data.List as L
94−#endif
9596
9697import Unsafe.Coerce
9798
99+import Obelisk.Configs
100+
98101infixr 5 :~
99102pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
100103pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
@@ -176,6 +179,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
176179 askQueryResult = lift askQueryResult
177180 queryIncremental = lift . queryIncremental
178181
182+instance HasConfigs m => HasConfigs (RoutedT t r m)
183+
179184instance (Monad m, RouteToUrl r m) => RouteToUrl r (QueryT t q m)
180185
181186instance (Monad m, SetRoute t r m) => SetRoute t r (QueryT t q m)
@@ -338,6 +343,8 @@ instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
338343 type PrimState (SetRouteT t r m) = PrimState m
339344 primitive = lift . primitive
340345
346+instance HasConfigs m => HasConfigs (SetRouteT t r m)
347+
341348instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
342349 runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
343350 traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
@@ -435,6 +442,8 @@ instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RouteToUrlT r m) where
435442 askQueryResult = lift askQueryResult
436443 queryIncremental = lift . queryIncremental
437444
445+instance HasConfigs m => HasConfigs (RouteToUrlT t m)
446+
438447runRouteViewT
439448 :: forall t m r a.
440449 ( TriggerEvent t m
@@ -445,17 +454,20 @@ runRouteViewT
445454 , MonadFix m
446455 )
447456 => (Encoder Identity Identity r PageName)
457+ --TODO: Get rid of the switchover and useHash arguments
458+ -- useHash can probably be baked into the encoder
448459 -> Event t () -- ^ Switchover event, nothing is done until this event fires. Used to prevent incorrect DOM expectations at hydration switchover time
460+ -> Bool
449461 -> RoutedT t r (SetRouteT t r (RouteToUrlT r m)) a
450462 -> m a
451−runRouteViewT routeEncoder switchover a = do
463+runRouteViewT routeEncoder switchover useHash a = do
452464 rec historyState <- manageHistory' switchover $ HistoryCommand_PushState <$> setState
453465 let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
454466 -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
455467 -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
456468 -- this ever to happen, we'll just handle it by failing completely with 'error'.
457469 route :: Dynamic t r
458− route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
470+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath useHash &&& uriQuery) . _historyItem_uri) historyState
459471 where
460472 errorLeft (Left e) = error (T.unpack e)
461473 errorLeft (Right x) = x
@@ -476,7 +488,7 @@ runRouteViewT routeEncoder switchover a = do
476488 -- we can change this function later to accommodate.
477489 -- See: https://github.com/whatwg/html/issues/2174
478490 , _historyStateUpdate_title = ""
479− , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
491+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath useHash newPath $ (_historyItem_uri currentHistoryState)
480492 { uriQuery = newQuery
481493 }
482494 }
@@ -503,25 +515,44 @@ routeLink r w = do
503515 setRoute $ r <$ domEvent Click e
504516 return a
505517
518+-- | A link widget that, when clicked, sets the route to current value of the
519+-- provided dynamic route. In non-javascript contexts the value of the dynamic post
520+-- build is used so the link still works like 'routeLink'.
521+dynRouteLink
522+ :: forall t m a route.
523+ ( DomBuilder t m
524+ , PostBuild t m
525+ , RouteToUrl (R route) m
526+ , SetRoute t (R route) m
527+ )
528+ => Dynamic t (R route) -- ^ Target route
529+ -> m a -- ^ Child widget
530+ -> m a
531+dynRouteLink dr w = do
532+ enc <- askRouteToUrl
533+ er <- dynamicAttributesToModifyAttributes $ ("href" =:) . enc <$> dr
534+ let cfg = (def :: ElementConfig EventResult t (DomBuilderSpace m))
535+ & elementConfig_eventSpec %~ addEventSpecFlags (Proxy :: Proxy (DomBuilderSpace m)) Click (\_ -> preventDefault)
536+ & elementConfig_modifyAttributes .~ er
537+ (e, a) <- element "a" cfg w
538+ setRoute $ tag (current dr) $ domEvent Click e
539+ return a
540+
506541-- On ios due to sandboxing when loading the page from a file adapt the
507542-- path to be based on the hash.
508543
509−adaptedUriPath :: URI -> String
510−#if defined(ios_HOST_OS)
511−adaptedUriPath = hashToPath . uriFragment
544+adaptedUriPath :: Bool -> URI -> String
545+adaptedUriPath = \case
546+ True -> hashToPath . uriFragment
547+ False -> uriPath
512548
513−hashToPath :: String -> String
514−hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
515−#else
516−adaptedUriPath = uriPath
517−#endif
518−
519−setAdaptedUriPath :: String -> URI -> URI
520−#if defined(ios_HOST_OS)
521−setAdaptedUriPath s u = u { uriFragment = pathToHash s }
549+setAdaptedUriPath :: Bool -> String -> URI -> URI
550+setAdaptedUriPath useHash s u = case useHash of
551+ True -> u { uriFragment = pathToHash s }
552+ False -> u { uriPath = s }
522553
523554pathToHash :: String -> String
524555pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
525−#else
526−setAdaptedUriPath s u = u { uriPath = s }
527−#endif
556+
557+hashToPath :: String -> String
558+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
lib/route/src/Obelisk/Route/TH.hs+4−5
@@ -1,8 +1,9 @@
11module Obelisk.Route.TH (deriveRouteComponent) where
22
3+import Data.Constraint.Extras.TH
34import Data.GADT.Show.TH
45import Data.GADT.Compare.TH
5−import Data.Universe.TH
6+import Data.Universe.Some.TH
67import Language.Haskell.TH
78
89-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
@@ -11,8 +12,6 @@ deriveRouteComponent x = concat <$> traverse ($ x)
1112 [ deriveGShow
1213 , deriveGEq
1314 , deriveGCompare
14− , deriveShowTagIdentity
15− , deriveEqTagIdentity
16− , deriveOrdTagIdentity
17− , deriveSomeUniverse
15+ , deriveUniverseSome
16+ , deriveArgDict
1817 ]
lib/run/obelisk-run.cabal+2−1
@@ -10,6 +10,7 @@ library
1010 attoparsec
1111 , base
1212 , bytestring
13+ , containers
1314 , cookie
1415 , dependent-sum
1516 , dependent-sum-template
@@ -25,7 +26,7 @@ library
2526 , network
2627 , obelisk-asset-serve-snap
2728 , obelisk-backend
28− , obelisk-executable-config
29+ , obelisk-executable-config-lookup
2930 , obelisk-frontend
3031 , obelisk-route
3132 , process
lib/run/src/Obelisk/Run.hs+52−36
@@ -20,18 +20,20 @@ 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)
28+import Data.Map (Map)
29+import qualified Data.Map as Map
2930import Data.Maybe
3031import Data.Semigroup ((<>))
3132import Data.Streaming.Network (bindPortTCP)
3233import Data.String (fromString)
3334import Data.Text (Text)
3435import qualified Data.Text as T
36+import qualified Data.Text.Encoding as T
3537import Language.Javascript.JSaddle.Run (syncPoint)
3638import Language.Javascript.JSaddle.WebSockets
3739import Network.HTTP.Client (Manager, defaultManagerSettings, newManager)
@@ -46,7 +48,6 @@ import Network.WebSockets (ConnectionOptions)
4648import Network.WebSockets.Connection (defaultConnectionOptions)
4749import qualified Obelisk.Asset.Serve.Snap as Snap
4850import Obelisk.Backend
49−import Obelisk.ExecutableConfig (get)
5051import Obelisk.Frontend
5152import Obelisk.Route.Frontend
5253import Reflex.Dom.Core
@@ -73,36 +74,39 @@ run port serveStaticAsset backend frontend = do
7374 case checkEncoder $ _backend_routeEncoder backend of
7475 Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
7576 Right validFullEncoder -> do
77+ publicConfigs <- getPublicConfigs
7678 backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
7779 _backend_run backend $ \serveRoute -> do
7880 runSnapWithCommandLineArgs $ do
7981 getRouteWith validFullEncoder >>= \case
8082 Identity r -> case r of
81− InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
82− InR obeliskRoute :=> Identity a ->
83− serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend $ obeliskRoute :/ a
83+ FullRoute_Backend backendRoute :/ a -> serveRoute $ backendRoute :/ a
84+ FullRoute_Frontend obeliskRoute :/ a ->
85+ serveDefaultObeliskApp (mkRouteToUrl validFullEncoder) serveStaticAsset frontend publicConfigs $ obeliskRoute :/ a
8486 let conf = defRunConfig { _runConfig_redirectPort = port }
85− runWidget conf frontend validFullEncoder `finally` killThread backendTid
87+ runWidget conf publicConfigs frontend validFullEncoder `finally` killThread backendTid
8688
8789-- Convenience wrapper to handle path segments for 'Snap.serveAsset'
8890runServeAsset :: FilePath -> [Text] -> Snap ()
8991runServeAsset rootPath = Snap.serveAsset "" rootPath . T.unpack . T.intercalate "/"
9092
91−getConfigRoute :: IO (Maybe URI)
92−getConfigRoute = get "config/common/route" >>= \case
93− Just r -> case URI.mkURI $ T.strip r of
94− Just route -> pure $ Just route
95− Nothing -> do
96− putStrLn $ "Route is invalid: " <> show r
97− pure Nothing
98− Nothing -> pure Nothing
99−
100−defAppUri :: URI
101−defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
102−
103−runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
104−runWidget conf frontend validFullEncoder = do
105− uri <- fromMaybe defAppUri <$> getConfigRoute
93+getConfigRoute :: Map Text ByteString -> Either Text URI
94+getConfigRoute configs = case Map.lookup "common/route" configs of
95+ Just r ->
96+ let stripped = T.strip (T.decodeUtf8 r)
97+ in case URI.mkURI stripped of
98+ Just route -> Right route
99+ Nothing -> Left $ "Couldn't parse route as URI; value read was: " <> T.pack (show stripped)
100+ Nothing -> Left $ "Couldn't find config file common/route; it should contain the site's canonical root URI" <> T.pack (show $ Map.keys configs)
101+
102+runWidget
103+ :: RunConfig
104+ -> Map Text ByteString
105+ -> Frontend (R frontendRoute)
106+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
107+ -> IO ()
108+runWidget conf configs frontend validFullEncoder = do
109+ uri <- either (fail . T.unpack) pure $ getConfigRoute configs
106110 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
107111 redirectHost = _runConfig_redirectHost conf
108112 redirectPort = _runConfig_redirectPort conf
@@ -114,42 +118,54 @@ runWidget conf frontend validFullEncoder = do
114118 close
115119 (\skt -> do
116120 man <- newManager defaultManagerSettings
117− app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
121+ app <- obeliskApp configs defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
118122 runSettingsSocket settings skt app)
119123
120124obeliskApp
121− :: forall route backendRoute. ConnectionOptions
122− -> Frontend (R route)
123− -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
125+ :: forall frontendRoute backendRoute
126+ . Map Text ByteString
127+ -> ConnectionOptions
128+ -> Frontend (R frontendRoute)
129+ -> Encoder Identity Identity (R (FullRoute backendRoute frontendRoute)) PageName
124130 -> URI
125131 -> Application
126132 -> IO Application
127−obeliskApp opts frontend validFullEncoder uri backend = do
128− let entryPoint = do
129− runFrontend validFullEncoder frontend
133+obeliskApp configs opts frontend validFullEncoder uri backend = do
134+ let mode = FrontendMode
135+ { _frontendMode_hydrate = True
136+ , _frontendMode_adjustRoute = False
137+ }
138+ entryPoint = do
139+ runFrontendWithConfigsAndCurrentRoute mode configs validFullEncoder frontend
130140 syncPoint
131141 jsaddlePath <- URI.mkPathPiece "jsaddle"
132142 let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
133143 Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
134144 jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
135− 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
136146 Identity r -> case r of
137− InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
147+ FullRoute_Frontend (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :/ jsaddleRoute -> case jsaddleRoute of
138148 JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
139149 _ -> flip jsaddle sendResponse $ req
140150 { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
141151 }
142− InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
152+ FullRoute_Frontend (ObeliskRoute_App appRouteComponent) :/ appRouteRest -> do
143153 let cookies = maybe [] parseCookies $ lookup (fromString "Cookie") (W.requestHeaders req)
144− html <- renderJsaddleFrontend cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
154+ html <- renderJsaddleFrontend configs cookies (mkRouteToUrl validFullEncoder) (appRouteComponent :/ appRouteRest) frontend
145155 sendResponse $ W.responseLBS H.status200 [("Content-Type", staticRenderContentType)] $ BSLC.fromStrict html
146156 _ -> backend req sendResponse
147157
148−renderJsaddleFrontend :: Cookies -> (route -> Text) -> route -> Frontend route -> IO ByteString
149−renderJsaddleFrontend cookies urlEnc r f =
158+renderJsaddleFrontend
159+ :: Map Text ByteString
160+ -> Cookies
161+ -> (route -> Text)
162+ -> route
163+ -> Frontend route
164+ -> IO ByteString
165+renderJsaddleFrontend configs cookies urlEnc r f =
150166 let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
151167 jsaddlePreload = elAttr "link" ("rel" =: "preload" <> "as" =: "script" <> "href" =: "/jsaddle/jsaddle.js") blank
152− in renderFrontendHtml cookies urlEnc r (_frontend_head f >> jsaddlePreload) (_frontend_body f >> jsaddleScript)
168+ in renderFrontendHtml configs cookies urlEnc r f jsaddlePreload jsaddleScript
153169
154170-- | like 'bindPortTCP' but reconnects on exception
155171bindPortTCPRetry :: Settings
lib/selftest/obelisk-selftest.cabal+1
@@ -16,6 +16,7 @@ library
1616 , http-types
1717 , HUnit
1818 , obelisk-cliapp
19+ , obelisk-executable-config-lookup
1920 , obelisk-run
2021 , process
2122 , network
lib/selftest/src/Obelisk/SelfTest.hs+5−2
@@ -34,6 +34,7 @@ import Test.HUnit.Base
3434
3535import Obelisk.CliApp hiding (runCli, readCreateProcessWithExitCode)
3636import qualified Obelisk.CliApp as CliApp
37+import Obelisk.ExecutableConfig.Lookup (getConfigs)
3738import Obelisk.Run (getConfigRoute)
3839
3940data ObRunState
@@ -127,8 +128,10 @@ main = do
127128 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
128129 ls tmp >>= liftIO . assertEqual "" []
129130
130− it "produces a valid route config" $ inTmpObInit $ \tmp -> do
131− liftIO $ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $ getConfigRoute `shouldNotReturn` Nothing
131+ it "produces a valid route config" $ inTmpObInit $ \tmp -> liftIO $ do
132+ configs <- getConfigs
133+ withCurrentDirectory (T.unpack $ toTextIgnore tmp) $
134+ return (either (const Nothing) Just $ getConfigRoute configs) `shouldNotReturn` Nothing
132135
133136 -- These tests fail with "Could not find module 'Obelisk.Generated.Static'"
134137 -- when not run by 'nix-build --attr selftest'
lib/tabulation/CHANGELOG.mdnew file+5
@@ -0,0 +1,5 @@
1+# Revision history for tabulation
2+
3+## 0.1.0.0 -- 2019-05-24
4+
5+* First version. Basically carve out this library as a place for the HasFields class.
lib/tabulation/LICENSEnew file+30
@@ -0,0 +1,30 @@
1+Copyright (c) 2019, Cale Gibbard
2+
3+All rights reserved.
4+
5+Redistribution and use in source and binary forms, with or without
6+modification, are permitted provided that the following conditions are met:
7+
8+ * Redistributions of source code must retain the above copyright
9+ notice, this list of conditions and the following disclaimer.
10+
11+ * Redistributions in binary form must reproduce the above
12+ copyright notice, this list of conditions and the following
13+ disclaimer in the documentation and/or other materials provided
14+ with the distribution.
15+
16+ * Neither the name of Cale Gibbard nor the names of other
17+ contributors may be used to endorse or promote products derived
18+ from this software without specific prior written permission.
19+
20+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
lib/tabulation/Setup.hsnew file+2
@@ -0,0 +1,2 @@
1+import Distribution.Simple
2+main = defaultMain
lib/tabulation/src/Data/Tabulation.hsnew file+21
@@ -0,0 +1,21 @@
1+{-# LANGUAGE TypeFamilies #-}
2+{-# LANGUAGE RankNTypes #-}
3+
4+module Data.Tabulation where
5+
6+import Control.Lens
7+
8+-- | This is a class for record types whose fields can be enumerated by an associated GADT. It's closely related to the concept of a representable functor, except without the functor part, and the fields are not all the same type.
9+class HasFields a where
10+ type Field a :: * -> *
11+
12+ fieldLens :: Field a x -> Lens' a x
13+ tabulateFieldsA :: Applicative f => (forall x. Field a x -> f x) -> f a
14+
15+ tabulateFields :: (forall x. Field a x -> x) -> a
16+ tabulateFields f = runIdentity (tabulateFieldsA (Identity . f))
17+ traverseWithField :: Applicative m => (forall x. Field a x -> x -> m x) -> a -> m a
18+ traverseWithField t r = tabulateFieldsA (\f -> t f (indexField r f))
19+ indexField :: a -> Field a x -> x
20+ indexField a f = a ^. fieldLens f
21+ {-# MINIMAL fieldLens, tabulateFieldsA #-}
lib/tabulation/tabulation.cabalnew file+26
@@ -0,0 +1,26 @@
1+cabal-version: >=1.10
2+-- Initial package description 'tabulation.cabal' generated by 'cabal
3+-- init'. For further documentation, see
4+-- http://haskell.org/cabal/users-guide/
5+
6+name: tabulation
7+version: 0.1.0.0
8+synopsis: A library for types that can be completely reconstructed by fields as specified by a GADT.
9+-- description:
10+-- bug-reports:
11+license: BSD3
12+license-file: LICENSE
13+author: Cale Gibbard
14+maintainer: cale.gibbard@obsidian.systems
15+-- copyright:
16+category: Data
17+build-type: Simple
18+extra-source-files: CHANGELOG.md
19+
20+library
21+ exposed-modules: Data.Tabulation
22+ -- other-modules:
23+ -- other-extensions:
24+ build-depends: base, lens
25+ hs-source-dirs: src
26+ default-language: Haskell2010
release.nix+17−6
@@ -1,4 +1,5 @@
1−{ local-self ? import ./. {}
1+{ self-args ? {}
2+, local-self ? import ./. self-args
23}:
34
45let
@@ -9,7 +10,7 @@ let
910 obeliskPackagesCommon = [
1011 "obelisk-frontend"
1112 "obelisk-route"
12− "obelisk-executable-config"
13+ "obelisk-executable-config-lookup"
1314 ];
1415
1516 obeliskPackagesBackend = obeliskPackagesCommon ++ [
@@ -59,17 +60,27 @@ let
5960 (concatDepends ghcjs)
6061 ];
6162 command = local-self.command;
62− serverExeSkeleton = (import ./skeleton {}).exe;
63− builtSkeleton = (import ./skeleton {}).all;
63+ serverExeSkeleton = (import ./skeleton { obelisk = local-self; }).exe;
64+ androidObelisk = import ./. (self-args // {
65+ config.android_sdk.accept_license = true;
66+ });
67+ androidSkeleton = (import ./skeleton { obelisk = androidObelisk; }).android.frontend;
68+ iosObelisk = import ./. (self-args // {
69+ system = "x86_64-darwin";
70+ iosSdkVersion = "10.2";
71+ });
72+ iosSkeleton = (import ./skeleton { obelisk = iosObelisk; }).ios.frontend;
6473 in {
6574 inherit
6675 command
6776 ghc ghcjs
68− serverExeSkeleton builtSkeleton;
77+ serverExeSkeleton
78+ iosSkeleton
79+ androidSkeleton;
6980 cache = reflex-platform.pinBuildInputs
7081 "obelisk-${system}"
7182 cachePackages
72− [command serverExeSkeleton builtSkeleton];
83+ [command serverExeSkeleton iosSkeleton androidSkeleton];
7384 });
7485
7586 metaCache = local-self.pinBuildInputs
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/Api.hs+1−1
@@ -1,4 +1,4 @@
11module Common.Api where
22
33commonStuff :: String
4−commonStuff = "Here is a string defined in code common to the frontend and backend."
4+commonStuff = "Here is a string defined in Common.Api"
skeleton/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/config/common/examplenew file+1
@@ -0,0 +1 @@
1+This string comes from config/common/example
skeleton/default.nix+9−3
@@ -1,7 +1,13 @@
1−{ system ? builtins.currentSystem # TODO: Get rid of this system cruft
2−, iosSdkVersion ? "10.2"
1+{ 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;
8+ }
39}:
4−with import ./.obelisk/impl { inherit system iosSdkVersion; };
10+with obelisk;
511project ./. ({ ... }: {
612 android.applicationId = "systems.obsidian.obelisk.examples.minimal";
713 android.displayName = "Obelisk Minimal Example";
skeleton/frontend/frontend.cabal+1
@@ -10,6 +10,7 @@ library
1010 , obelisk-frontend
1111 , obelisk-route
1212 , reflex-dom
13+ , obelisk-executable-config-lookup
1314 , obelisk-generated-static
1415 , text
1516 exposed-modules:
skeleton/frontend/src-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+8−1
@@ -4,7 +4,9 @@
44module Frontend where
55
66import qualified Data.Text as T
7+import qualified Data.Text.Encoding as T
78import Obelisk.Frontend
9+import Obelisk.Configs
810import Obelisk.Route
911import Reflex.Dom.Core
1012
@@ -12,7 +14,6 @@ import Common.Api
1214import Common.Route
1315import Obelisk.Generated.Static
1416
15−
1617frontend :: Frontend (R FrontendRoute)
1718frontend = Frontend
1819 { _frontend_head = el "title" $ text "Obelisk Minimal Example"
@@ -20,4 +21,10 @@ frontend = Frontend
2021 text "Welcome to Obelisk!"
2122 el "p" $ text $ T.pack commonStuff
2223 elAttr "img" ("src" =: static @"obelisk.jpg") blank
24+ el "div" $ do
25+ exampleConfig <- getConfig "common/example"
26+ case exampleConfig of
27+ Nothing -> text "No config file found in config/common/example"
28+ Just s -> text (T.decodeUtf8 s)
29+ return ()
2330 }