on master help sign in
Merge branch 'master' into rename-generated-static-module
commit41bb177b200503fefc79caba6eab5f37f16b344f authorLuigy Leon <luichi.19@gmail.com> dateThu 11 Oct 2018, 15:22 (8 years ago) parentcba5957b462e48
50 files changed+2068−341
default.nix+148−127 dep/.gitignore+4 dep/reflex-platform/github.json+2−2 lib/asset/manifest/src/Obelisk/Asset/Gather.hs−1 lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+8−3 lib/backend/obelisk-backend.cabal+11−1 lib/backend/src/Obelisk/Backend.hs+130−24 lib/cliapp/src/Obelisk/CliApp.hs+1 lib/cliapp/src/Obelisk/CliApp/Process.hs+13−4 lib/command/obelisk-command.cabal+1 lib/command/src/Obelisk/App.hs+4−3 lib/command/src/Obelisk/Command.hs+3−1 lib/command/src/Obelisk/Command/Deploy.hs+2−2 lib/command/src/Obelisk/Command/Nix.hs+4 lib/command/src/Obelisk/Command/Project.hs+1−2 lib/command/src/Obelisk/Command/Run.hs+86−32 lib/command/src/Obelisk/Command/Thunk.hs−1 lib/command/src/Obelisk/Command/Upgrade.hs+1−1 lib/executable-config/README.md+2−7 lib/executable-config/default.nix+2−1 lib/executable-config/inject/obelisk-executable-config-inject.cabal+2 lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+40−7 lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hs+1−1 lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hs+1−1 lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hs+1−1 lib/frontend/obelisk-frontend.cabal+22 lib/frontend/src/Obelisk/Frontend.hs+165 lib/route/obelisk-route.cabal+36 lib/route/src/Obelisk/Route.hs+795 lib/route/src/Obelisk/Route/Frontend.hs+322 lib/route/src/Obelisk/Route/TH.hs+18 lib/run/obelisk-run.cabal+13−1 lib/run/src/Obelisk/Run.hs+79−36 migration/07900e6c41ab9a78917f66fcf7e0d9b8-8c84f81d9b09a5e0f028f3c6341ffb36/obelisk-handoff migration/07900e6c41ab9a78917f66fcf7e0d9b8-8c84f81d9b09a5e0f028f3c6341ffb36/obelisk-upgrade+15 release.nix+28−27 scripts/ghcid-this-lib+4 skeleton/.obelisk/.gitignore+3 skeleton/backend/backend.cabal+2−6 skeleton/backend/src-bin/main.hs+6 skeleton/backend/src/Backend.hs+11−6 skeleton/backend/src/main.hs−20 skeleton/common/common.cabal+4 skeleton/common/src/Common/Api.hs+1−1 skeleton/common/src/Common/Route.hs+52 skeleton/devel/Devel.hs−10 skeleton/frontend/frontend.cabal+5−2 skeleton/frontend/src-bin/main.hs+10 skeleton/frontend/src/Frontend.hs+9−5 skeleton/frontend/src/main.hs−5
default.nix+148−127
@@ -5,8 +5,8 @@
55let
66 getReflexPlatform = sys: import ./dep/reflex-platform { inherit iosSdkVersion; system = sys; };
77 reflex-platform = getReflexPlatform system;
8− inherit (reflex-platform) hackGet;
9− pkgs = reflex-platform.nixpkgs;
8+ inherit (reflex-platform) hackGet nixpkgs;
9+ pkgs = nixpkgs;
1010in with pkgs.haskell.lib; with pkgs.lib;
1111let
1212 # TODO: Remove this after updating nixpkgs: https://github.com/NixOS/nixpkgs/issues/37750
@@ -52,53 +52,28 @@ let
5252 enableLibraryProfiling = profiling;
5353 });
5454
55− #TODO: Eliminate this when https://github.com/phadej/github/pull/307 makes its way to reflex-platform
56− github = overrideCabal super.github (drv: {
57− src = pkgs.fetchFromGitHub {
58− owner = "ryantrinkle";
59− repo = "github";
60− rev = "8f543cdc07876bfb7b924d3722e3dbc1df4b02ca";
61− sha256 = "0vcnx9cxqd821kmjx1r4cvj95zs742qm1pwqnb52vw3djplbqd86";
62− };
63− sha256 = null;
64− revision = null;
65− editedCabalFile = null;
66− });
67−
6855 # Dynamic linking with split objects dramatically increases startup time (about 0.5 seconds on a decent machine with SSD)
6956 obelisk-command = addOptparseApplicativeCompletionScripts "ob" (justStaticExecutables' super.obelisk-command);
70−
71− optparse-applicative = self.callHackage "optparse-applicative" "0.14.0.0" {};
7257 });
7358 };
7459
7560 fixUpstreamPkgs = self: super: {
76− heist = doJailbreak super.heist; #TODO: Move up to reflex-platform; create tests for r-p supported packages
77− modern-uri =
78− let src = pkgs.fetchFromGitHub {
79− owner = "mrkkrp";
80− repo = "modern-uri";
81− rev = "21064285deb284cb3328094c69c34f9f67919cc9";
82− sha256 = "0vddw8r9sb31h1fz1anzxrs9p3a3p8ygpxlj398z5j47wmr86cmi";
83− };
84− in (overrideCabal (self.callCabal2nix "modern-uri" src {}) (drv: {
85− doCheck = false;
86− postPatch = (drv.postPatch or "") + ''
87− substituteInPlace Text/URI/Types.hs \
88− --replace "instance Arbitrary (NonEmpty (RText 'PathPiece)) where" "" \
89− --replace " arbitrary = (:|) <$> arbitrary <*> arbitrary" ""
90− '';
91− })).override { megaparsec = super.megaparsec_6_1_1; };
92− algebraic-graphs =
93− let src = pkgs.fetchFromGitHub {
94− owner = "snowleopard";
95− repo = "alga";
96− rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
97− sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
98− };
99− in pkgs.haskell.lib.dontCheck (self.callCabal2nix "algebraic-graphs" src {});
100− network-transport = self.callHackage "network-transport" "0.5.2" {};
101− network-transport-tcp = self.callHackage "network-transport-tcp" "0.6.0" {};
61+ algebraic-graphs = pkgs.haskell.lib.doJailbreak
62+ (self.callCabal2nix "algebraic-graphs" (pkgs.fetchFromGitHub {
63+ owner = "snowleopard";
64+ repo = "alga";
65+ rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
66+ sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
67+ }) {});
68+
69+ # Need deriveSomeUniverse
70+ # PR: https://github.com/dmwit/universe/pull/32
71+ universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
72+ owner = "obsidiansystems";
73+ repo = "universe";
74+ rev = "5a2fc823caa4163411d7e41aa80e67cefb15944a";
75+ sha256 = "0ll2z0fh18z6x8jl8kbp7ldagwccz3wjmvrw1gw752z058n82yfa";
76+ } + /template) {};
10277 };
10378
10479 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -113,16 +88,33 @@ let
11388 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
11489 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
11590 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
116− obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).override { Cabal = super.Cabal_2_0_0_2; };
91+ obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).overrideAttrs
92+ (drv: {
93+ buildInputs = drv.buildInputs ++ [ pkgs.makeWrapper ];
94+ postInstall = ''
95+ ${drv.postInstall or ""}
96+ # Install migrations
97+ cp -r ${./migration} $out/migration;
98+ '';
99+ postFixup = ''
100+ ${drv.postFixup or ""}
101+ # Make `ob` reference its runtime dependencies.
102+ wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
103+ '';
104+ });
117105 obelisk-executable-config = executableConfig.haskellPackage self;
118− obelisk-executable-config-inject = executableConfig.platforms.web.inject self; # TODO handle platforms.{ios,android}
106+ obelisk-executable-config-inject = executableConfig.platforms.web.inject self;
107+ obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
119108 obelisk-migration = self.callCabal2nix "obelisk-migration" (cleanSource ./lib/migration) {};
120109 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
110+ obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
121111 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
122112 obelisk-snap = self.callCabal2nix "obelisk-snap" (cleanSource ./lib/snap) {};
123113 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
124114 };
125115
116+ inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
117+
126118 defaultHaskellOverrides = composeExtensions fixUpstreamPkgs addLibs;
127119in
128120with pkgs.lib;
@@ -132,16 +124,7 @@ rec {
132124 pathGit = ./.; # Used in CI by the migration graph hash algorithm to correctly ignore files.
133125 path = reflex-platform.filterGit ./.;
134126 obelisk = ghcObelisk;
135− commandWithMigration = ghcObelisk.obelisk-command.overrideAttrs (drv: {
136− postInstall = (drv.postInstall or "") +
137− ''cp -r ${./migration} $out/migration;'';
138− });
139− command = pkgs.runCommand commandWithMigration.name { nativeBuildInputs = [pkgs.makeWrapper]; } ''
140− mkdir -p "$out/bin"
141− ln -s '${commandWithMigration}/bin/ob' "$out/bin/ob"
142− wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
143− '';
144− commandEnv = ghcObelisk.obelisk-command.env;
127+ command = ghcObelisk.obelisk-command;
145128 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
146129
147130 selftest = pkgs.writeScript "selftest" ''
@@ -174,83 +157,109 @@ rec {
174157 '';
175158
176159 compressedJs = frontend: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
177− mkdir $out
178− cd $out
179− ln -s "${justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
180− closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ADVANCED --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
181− echo "//# sourceMappingURL=all.js.map" >> all.js
160+ mkdir $out
161+ cd $out
162+ ln -s "${justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
163+ closure-compiler --externs "${reflex-platform.ghcjsExternsJs}" -O ADVANCED --jscomp_warning=checkVars --create_source_map="all.js.map" --source_map_format=V3 --js_output_file="all.js" all.unminified.js
164+ echo "//# sourceMappingURL=all.js.map" >> all.js
182165 '';
183166
167+ serverModules = {
168+ mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
169+ imports = [
170+ (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
171+ ];
172+ networking = {
173+ inherit hostName;
174+ firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
175+ };
176+ security.acme.certs = if enableHttps then {
177+ "${routeHost}".email = adminEmail;
178+ } else {};
179+ ec2.hvm = true;
180+ };
181+
182+ mkObeliskApp =
183+ { exe
184+ , routeHost
185+ , enableHttps
186+ , name ? "backend"
187+ , user ? name
188+ , group ? user
189+ , baseUrl ? "/"
190+ , internalPort ? 8000
191+ , backendArgs ? "--port=${toString internalPort}"
192+ , ...
193+ }: {...}: {
194+ services.nginx = {
195+ enable = true;
196+ virtualHosts."${routeHost}" = {
197+ enableACME = enableHttps;
198+ forceSSL = enableHttps;
199+ locations.${baseUrl} = {
200+ proxyPass = "http://localhost:" + toString internalPort;
201+ };
202+ };
203+ };
204+ systemd.services.${name} = {
205+ wantedBy = [ "multi-user.target" ];
206+ after = [ "network.target" ];
207+ restartIfChanged = true;
208+ script = ''
209+ ln -sft . '${exe}'/*
210+ mkdir -p log
211+ exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
212+ '';
213+ serviceConfig = {
214+ User = user;
215+ KillMode = "process";
216+ WorkingDirectory = "~";
217+ Restart = "always";
218+ RestartSec = 5;
219+ };
220+ };
221+ users = {
222+ users.${user} = {
223+ description = "${user} service";
224+ home = "/var/lib/${user}";
225+ createHome = true;
226+ isSystemUser = true;
227+ group = group;
228+ };
229+ groups.${group} = {};
230+ };
231+ };
232+ };
233+
184234 serverExe = backend: frontend: assets: config:
185235 pkgs.runCommand "serverExe" {} ''
186236 mkdir $out
187237 set -eux
188− ln -s "${justStaticExecutables backend}"/bin/backend $out/backend
189− ln -s "${assets}" $out/static
190− ln -s "${config}" $out/config
191− ln -s ${compressedJs frontend} $out/frontend.jsexe
192− ''; #TODO: run frontend.jsexe through the asset processing pipeline
238+ ln -s "${justStaticExecutables backend}"/bin/* $out/
239+ ln -s "${mkAssets assets}" $out/static.assets
240+ cp -r ${config} $out/config
241+ ln -s ${mkAssets (compressedJs frontend)} $out/frontend.jsexe.assets
242+ '';
193243
194− server = { exe, hostName, adminEmail, routeHost, enableHttps }:
195− let system = "x86_64-linux";
196− nixos = import (pkgs.path + /nixos);
197− backendPort = 8000;
244+ server = { exe, hostName, adminEmail, routeHost, enableHttps, config }@args:
245+ let
246+ nixos = import (pkgs.path + /nixos);
198247 in nixos {
199− inherit system;
200− configuration = args: {
248+ system = "x86_64-linux";
249+ configuration = {
201250 imports = [
202− (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
251+ (serverModules.mkBaseEc2 args)
252+ (serverModules.mkObeliskApp args)
203253 ];
204− networking = {
205− inherit hostName;
206− firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
207− };
208− services.nginx = {
209− enable = true;
210− virtualHosts."${routeHost}" = {
211− enableACME = enableHttps;
212− forceSSL = enableHttps;
213− locations."/" = {
214− proxyPass = "http://localhost:" + toString backendPort;
215− };
216− };
217− };
218− security.acme.certs = if enableHttps then {
219− "${routeHost}".email = adminEmail;
220− } else { };
221− systemd.services.backend = {
222− wantedBy = [ "multi-user.target" ];
223− after = [ "network.target" ];
224− restartIfChanged = true;
225− script = ''
226− ln -sft . "${exe}"/*
227− mkdir -p log
228− exec ./backend >>backend.out 2>>backend.err </dev/null
229− '';
230− serviceConfig = {
231− User = "backend";
232− KillMode = "process";
233− WorkingDirectory = "~";
234− Restart = "always";
235− RestartSec = 5;
236− };
237− };
238− users.extraUsers.backend = {
239− description = "backend service";
240− home = "/var/lib/backend";
241− createHome = true;
242− isSystemUser = true;
243− group = "backend";
244− };
245− ec2.hvm = true;
246254 };
247255 };
248256
249257
250258 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
251259 project = base: projectDefinition:
252− let assets = processAssets { src = base + "/static"; };
253− configPath = base + "/config";
260+ let configPath = base + "/config";
261+ static = base + "/static";
262+ processedStatic = processAssets { src = static; };
254263 projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
255264 let mkProject = { android ? null #TODO: Better error when missing
256265 , ios ? null #TODO: Better error when missing
@@ -265,7 +274,6 @@ rec {
265274 commonName = "common";
266275 staticName = "static";
267276 staticPath = base + "/static";
268− assets = processAssets { src = base + "/static"; };
269277 # The packages whose names and roles are defined by this package
270278 predefinedPackages = filterAttrs (_: x: x != null) {
271279 ${frontendName} = nullIfAbsent (base + "/frontend");
@@ -274,15 +282,25 @@ rec {
274282 };
275283 combinedPackages = predefinedPackages // packages;
276284 projectOverrides = self: super: {
277− ${staticName} = dontHaddock (self.callCabal2nix "static" assets.haskellManifest {});
285+ ${staticName} = dontHaddock (self.callCabal2nix "static" processedStatic.haskellManifest {});
278286 ${backendName} = addBuildDepend super.${backendName} self.obelisk-run;
279287 };
280288 totalOverrides = composeExtensions (composeExtensions defaultHaskellOverrides projectOverrides) overrides;
289+ inherit (nixpkgs) lib;
290+ inherit (lib.strings) hasPrefix;
291+ privateConfigDirs = ["config/backend"];
292+ injectableConfig = builtins.filterSource (path: _:
293+ !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
294+ ) configPath;
281295 in {
282296 inherit shellToolOverrides tools withHoogle;
283297 overrides = totalOverrides;
284298 packages = combinedPackages;
285299 shells = {
300+ ghcSavedSplices = (filter (x: hasAttr x combinedPackages) [
301+ commonName
302+ frontendName
303+ ]);
286304 ghc = (filter (x: hasAttr x combinedPackages) [
287305 backendName
288306 commonName
@@ -296,25 +314,28 @@ rec {
296314 android = {
297315 ${if android == null then null else frontendName} = {
298316 executableName = "frontend";
299− ${if builtins.pathExists staticPath then "assets" else null} = assets.symlinked;
317+ ${if builtins.pathExists staticPath then "assets" else null} =
318+ executableConfig.platforms.android.inject injectableConfig processedStatic.symlinked;
300319 } // android;
301320 };
302321 ios = {
303322 ${if ios == null then null else frontendName} = {
304323 executableName = "frontend";
305− ${if builtins.pathExists staticPath then "staticSrc" else null} = assets.symlinked;
324+ ${if builtins.pathExists staticPath then "staticSrc" else null} =
325+ executableConfig.platforms.ios.inject injectableConfig processedStatic.symlinked;
306326 } // ios;
307327 };
308328 };
309329 in mkProject (projectDefinition args));
310− serverOn = sys:
311− serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend assets.symlinked configPath;
312− linuxExe = serverOn "x86_64-linux";
330+ serverOn = sys: config: serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend static config;
331+ # `exe` is project's backend executable, with frontend assets, config, etc.
332+ # `linuxExe` is the same but built for x86_64-linux.
333+ exe = serverOn system configPath;
334+ linuxExe = serverOn "x86_64-linux" configPath;
313335 in projectOut system // {
314− inherit linuxExe;
315− exe = serverOn system;
316− server = args@{ hostName, adminEmail, routeHost, enableHttps }:
317− server (args // { exe = linuxExe;});
336+ inherit exe linuxExe;
337+ server = args@{ hostName, adminEmail, routeHost, enableHttps, config }:
338+ server (args // { exe = serverOn "x86_64-linux" config;});
318339 obelisk = import (base + "/.obelisk/impl") {};
319340 };
320341 haskellPackageSets = {
dep/.gitignorenew file+4
@@ -0,0 +1,4 @@
1+*
2+!.gitignore
3+!*/*.json
4+!*/default.nix
dep/reflex-platform/github.json+2−2
@@ -2,6 +2,6 @@
22 "owner": "reflex-frp",
33 "repo": "reflex-platform",
44 "branch": "develop",
5− "rev": "3fae704d4790a2b918f3feb0a16293f4adc42e00",
6− "sha256": "0xgkw7q4racc18xcvvgbd9k6ikgfy1nfsls75xx6gi04kbj4z52l"
5+ "rev": "2f09cea8aea453969172fe1f499905af5afb4719",
6+ "sha256": "1v7v2fwahq4mcywjvg1i44jyqimpxd4kgn4x4a24lcdgw9yll9wg"
77}
lib/asset/manifest/src/Obelisk/Asset/Gather.hs−1
@@ -13,7 +13,6 @@ import Data.Char
1313import Data.Digest.Pure.SHA
1414import Data.Map (Map)
1515import qualified Data.Map as Map
16−import Data.Monoid
1716import qualified Data.Text as T
1817import Data.Text.Encoding
1918import qualified Data.Vector.Unboxed as UV
lib/asset/manifest/src/Obelisk/Asset/Promoted.hs+8−3
@@ -19,7 +19,6 @@ import qualified Data.Sequence as Seq
1919import Control.Monad.Trans.Writer
2020import System.FilePath
2121import System.Directory
22−import Data.Monoid
2322import Data.Text (Text)
2423import qualified Data.Text as T
2524import qualified Data.Text.IO as T
@@ -68,11 +67,17 @@ staticModuleFile moduleName paths = do
6867 , "{-# LANGUAGE FlexibleInstances #-}"
6968 , "{-# LANGUAGE KindSignatures #-}"
7069 , "{-# LANGUAGE OverloadedStrings #-}"
70+ , "{-# LANGUAGE ScopedTypeVariables #-}"
71+ , "{-# LANGUAGE TypeApplications #-}"
7172 , "module " <> moduleName <> " where"
7273 , ""
7374 , "import qualified GHC.Types"
74− , "import Data.Text ()"
75+ , "import Data.Text (Text)"
7576 , "import qualified Data.Text.Internal"
77+ , "import Data.Monoid ((<>))"
78+ , ""
79+ , "static :: forall a. StaticFile a => Text"
80+ , "static = \"static/\" <> hashedPath @a" --TODO: Use obelisk-route to generate this in a more consistent way
7681 , ""
7782 , T.pack $ pprint decs
7883 ]
@@ -101,7 +106,7 @@ staticClass :: WriterT (Seq Dec) Q StaticContext
101106staticClass = do
102107 let n x = Name (OccName x) NameS
103108 className = n "StaticFile"
104− methodName = n "static"
109+ methodName = n "hashedPath"
105110 cls = ClassD [] className [KindedTV (n "s") (ConT ''Symbol)] [] [SigD methodName (ConT ''Text)]
106111 tell $ Seq.singleton cls
107112 return $ StaticContext
lib/backend/obelisk-backend.cabal+11−1
@@ -7,11 +7,21 @@ library
77 hs-source-dirs: src
88 build-depends: base,
99 bytestring,
10+ categories,
1011 data-default,
12+ dependent-sum,
13+ dependent-sum-template,
1114 lens,
15+ mtl,
1216 obelisk-asset-serve-snap,
17+ obelisk-executable-config-inject,
18+ obelisk-frontend,
19+ obelisk-route,
1320 obelisk-snap,
1421 reflex-dom,
1522 snap,
16− snap-server
23+ snap-server,
24+ text,
25+ universe
1726 exposed-modules: Obelisk.Backend
27+ ghc-options: -Wall
lib/backend/src/Obelisk/Backend.hs+130−24
@@ -1,49 +1,155 @@
1+{-# LANGUAGE FlexibleContexts #-}
2+{-# LANGUAGE FlexibleInstances #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE KindSignatures #-}
5+{-# LANGUAGE LambdaCase #-}
6+{-# LANGUAGE MultiParamTypeClasses #-}
17{-# LANGUAGE OverloadedStrings #-}
8+{-# LANGUAGE TemplateHaskell #-}
29module Obelisk.Backend
3− ( backend
4− , BackendConfig (..)
10+ ( Backend (..)
511 -- * Re-exports
612 , Default (def)
13+ , getPageName
14+ , getRouteWith
15+ , runSnapWithCommandLineArgs
16+ , serveDefaultObeliskApp
17+ , prettifyOutput
18+ , runBackend
719 ) where
820
9−import Control.Lens
21+import Prelude hiding (id, (.))
22+import Control.Category
23+import Control.Monad
24+import Control.Monad.Except
25+import Control.Categorical.Bifunctor
26+import Data.ByteString (ByteString)
1027import qualified Data.ByteString.Char8 as BSC8
1128import Data.Default (Default (..))
12−import Obelisk.Asset.Serve.Snap (serveAssets)
13−import Obelisk.Snap
29+import Data.Dependent.Sum
30+import Data.Functor.Sum
31+import Data.Functor.Identity
32+import Data.Text (Text)
33+import qualified Data.Text as T
34+import Data.Text.Encoding
35+import Obelisk.Asset.Serve.Snap (serveAsset)
36+import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
37+import Obelisk.Frontend
38+import Obelisk.Route
1439import Reflex.Dom
15−import System.IO (hSetBuffering, stderr, BufferMode (..))
16−import Snap (httpServe, defaultConfig, commandLineConfig, route)
40+import Snap (MonadSnap, Snap, commandLineConfig, defaultConfig, getsRequest, httpServe, rqPathInfo,
41+ rqQueryString, writeBS, writeText)
1742import Snap.Internal.Http.Server.Config (Config (accessLog, errorLog), ConfigLog (ConfigIoLog))
43+import System.IO (BufferMode (..), hSetBuffering, stderr, stdout)
1844
19−--TODO: Add a link to a large explanation of the idea of using 'def'
20−-- | Configure the operation of the Obelisk backend. For reasonable defaults,
21−-- use 'def'.
22−data BackendConfig = BackendConfig
23− { _backendConfig_head :: StaticWidget () ()
45+data Backend backendRoute frontendRoute = Backend
46+ { _backend_routeEncoder :: Encoder (Either Text) Identity (R (Sum backendRoute (ObeliskRoute frontendRoute))) PageName
47+ , _backend_run :: ((R backendRoute -> Snap ()) -> IO ()) -> IO ()
2448 }
2549
26−instance Default BackendConfig where
27− def = BackendConfig (return ())
50+-- | The static assets provided must contain a compiled GHCJS app that corresponds exactly to the Frontend provided
51+data GhcjsApp route = GhcjsApp
52+ { _ghcjsApp_compiled :: !StaticAssets
53+ , _ghcjsApp_value :: !(Frontend route)
54+ }
55+
56+-- | Serve a frontend, which must be the same frontend that Obelisk has built and placed in the default location
57+--TODO: The frontend should be provided together with the asset paths so that this isn't so easily breakable; that will probably make this function obsolete
58+serveDefaultObeliskApp :: MonadSnap m => Frontend (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
59+serveDefaultObeliskApp frontend = serveObeliskApp defaultStaticAssets frontendApp
60+ where frontendApp = GhcjsApp
61+ { _ghcjsApp_compiled = defaultFrontendGhcjsAssets
62+ , _ghcjsApp_value = frontend
63+ }
2864
29−-- | Start an Obelisk backend
30−backend :: BackendConfig -> IO ()
31−backend cfg = do
65+prettifyOutput :: IO ()
66+prettifyOutput = do
3267 -- Make output more legible by decreasing the likelihood of output from
3368 -- multiple threads being interleaved
69+ hSetBuffering stdout LineBuffering
3470 hSetBuffering stderr LineBuffering
3571
72+defaultStaticAssets :: StaticAssets
73+defaultStaticAssets = StaticAssets
74+ { _staticAssets_processed = "static.assets"
75+ , _staticAssets_unprocessed = "static"
76+ }
77+
78+defaultFrontendGhcjsAssets :: StaticAssets
79+defaultFrontendGhcjsAssets = StaticAssets
80+ { _staticAssets_processed = "frontend.jsexe.assets"
81+ , _staticAssets_unprocessed = "frontend.jsexe"
82+ }
83+
84+runSnapWithCommandLineArgs :: Snap () -> IO ()
85+runSnapWithCommandLineArgs a = do
3686 -- Get the web server configuration from the command line
3787 cmdLineConf <- commandLineConfig defaultConfig
38− headHtml <- fmap snd $ renderStatic $ _backendConfig_head cfg
3988 let httpConf = cmdLineConf
4089 { accessLog = Just $ ConfigIoLog BSC8.putStrLn
4190 , errorLog = Just $ ConfigIoLog BSC8.putStrLn
4291 }
43− appCfg = def & appConfig_initialHead .~ headHtml
4492 -- Start the web server
45− httpServe httpConf $ route
46− [ ("", serveApp "" appCfg)
47− , ("", serveAssets "frontend.jsexe.assets" "frontend.jsexe") --TODO: Can we prevent naming conflicts between frontend.jsexe and static?
48− , ("", serveAssets "static.assets" "static")
49− ]
93+ httpServe httpConf a
94+
95+getPageName :: (MonadSnap m) => m PageName
96+getPageName = do
97+ p <- getsRequest rqPathInfo
98+ q <- getsRequest rqQueryString
99+ let pageNameEncoder' :: Encoder Identity Identity PageName (String, String)
100+ pageNameEncoder' = bimap
101+ (unpackTextEncoder . pathSegmentsTextEncoder . listToNonEmptyEncoder)
102+ (unpackTextEncoder . queryParametersTextEncoder . toListMapEncoder)
103+ return $ decode pageNameEncoder' (T.unpack (decodeUtf8 p), T.unpack (decodeUtf8 q))
104+
105+getRouteWith :: (MonadSnap m) => Encoder Identity parse route PageName -> m (parse route)
106+getRouteWith e = do
107+ pageName <- getPageName
108+ return $ tryDecode e pageName
109+
110+serveObeliskApp :: MonadSnap m => StaticAssets -> GhcjsApp (R appRoute) -> R (ObeliskRoute appRoute) -> m ()
111+serveObeliskApp staticAssets frontendApp = \case
112+ ObeliskRoute_App appRouteComponent :=> Identity appRouteRest -> serveGhcjsApp frontendApp $ GhcjsAppRoute_App appRouteComponent :/ appRouteRest
113+ ObeliskRoute_Resource resComponent :=> Identity resRest -> case resComponent :=> Identity resRest of
114+ ResourceRoute_Static :=> Identity pathSegments -> serveStaticAssets staticAssets pathSegments
115+ ResourceRoute_Ghcjs :=> Identity pathSegments -> serveGhcjsApp frontendApp $ GhcjsAppRoute_Resource :/ pathSegments
116+ ResourceRoute_JSaddleWarp :=> Identity _ -> do
117+ let msg = "Error: Obelisk.Backend received jsaddle request"
118+ liftIO $ putStrLn $ T.unpack msg
119+ writeText msg
120+
121+serveStaticAssets :: MonadSnap m => StaticAssets -> [Text] -> m ()
122+serveStaticAssets assets pathSegments = serveAsset (_staticAssets_processed assets) (_staticAssets_unprocessed assets) $ T.unpack $ T.intercalate "/" pathSegments
123+
124+data StaticAssets = StaticAssets
125+ { _staticAssets_processed :: !FilePath
126+ , _staticAssets_unprocessed :: !FilePath
127+ }
128+ deriving (Show, Read, Eq, Ord)
129+
130+data GhcjsAppRoute :: (* -> *) -> * -> * where
131+ GhcjsAppRoute_App :: appRouteComponent a -> GhcjsAppRoute appRouteComponent a
132+ GhcjsAppRoute_Resource :: GhcjsAppRoute appRouteComponent [Text]
133+
134+--TODO: Don't assume we're being served at "/"
135+serveGhcjsApp :: MonadSnap m => GhcjsApp (R appRouteComponent) -> R (GhcjsAppRoute appRouteComponent) -> m ()
136+serveGhcjsApp app = \case
137+ GhcjsAppRoute_App appRouteComponent :=> Identity appRouteRest ->
138+ writeBS <=< liftIO $ renderGhcjsFrontend (appRouteComponent :/ appRouteRest) $ _ghcjsApp_value app
139+ GhcjsAppRoute_Resource :=> Identity pathSegments -> serveStaticAssets (_ghcjsApp_compiled app) pathSegments
140+
141+runBackend :: Backend fullRoute frontendRoute -> Frontend (R frontendRoute) -> IO ()
142+runBackend backend frontend = case checkEncoder $ _backend_routeEncoder backend of
143+ Left e -> fail $ "backend error:\n" <> T.unpack e
144+ Right validFullEncoder -> _backend_run backend $ \serveRoute -> do
145+ runSnapWithCommandLineArgs $ do
146+ getRouteWith validFullEncoder >>= \case
147+ Identity r -> case r of
148+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
149+ InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp frontend $ obeliskRoute :/ a
150+
151+renderGhcjsFrontend :: route -> Frontend route -> IO ByteString
152+renderGhcjsFrontend route f = do
153+ let baseTag = elAttr "base" ("href" =: "/") blank --TODO: Figure out the base URL from the routes
154+ ghcjsScript = elAttr "script" ("language" =: "javascript" <> "src" =: "ghcjs/all.js" <> "defer" =: "defer") blank
155+ renderFrontendHtml route (_frontend_head f >> injectExecutableConfigs >> baseTag) (_frontend_body f >> ghcjsScript)
lib/cliapp/src/Obelisk/CliApp.hs+1
@@ -12,6 +12,7 @@ module Obelisk.CliApp
1212 , CliConfig
1313 , HasCliConfig
1414 , getCliConfig
15+ , Output
1516
1617 -- .Spinner
1718 , withSpinner
lib/cliapp/src/Obelisk/CliApp/Process.hs+13−4
@@ -20,7 +20,7 @@ module Obelisk.CliApp.Process
2020
2121import Control.Applicative (liftA2)
2222import Control.Monad (void, (<=<))
23−import Control.Monad.Catch (Exception, MonadMask, throwM)
23+import Control.Monad.Catch (Exception(..), MonadMask, throwM)
2424import Control.Monad.IO.Class (MonadIO, liftIO)
2525import qualified Data.ByteString.Char8 as BSC
2626import Data.Function (fix)
@@ -44,7 +44,9 @@ import Obelisk.CliApp.Types (Cli)
4444data ProcessFailed = ProcessFailed Process.CmdSpec Int -- exit code
4545 deriving Show
4646
47−instance Exception ProcessFailed
47+instance Exception ProcessFailed where
48+ displayException (ProcessFailed spec exitCode) =
49+ unwords ["ProcessFailed", reconstructCommand spec, show exitCode]
4850
4951readProcessAndLogStderr
5052 :: (MonadIO m, MonadMask m, Cli m)
@@ -77,7 +79,7 @@ createProcess
7779 :: (MonadIO m, Cli m)
7880 => CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
7981createProcess p = do
80− putLog Debug $ "Creating process: " <> T.pack (show $ cmdspec p)
82+ putLog Debug $ "Creating process: " <> T.pack (reconstructCommand (cmdspec p))
8183 liftIO $ Process.createProcess p
8284
8385-- | Like `System.Process.createProcess_` but also logs (debug) the process being run
@@ -85,7 +87,7 @@ createProcess_
8587 :: (MonadIO m, Cli m)
8688 => String -> CreateProcess -> m (Maybe Handle, Maybe Handle, Maybe Handle, ProcessHandle)
8789createProcess_ name p = do
88− putLog Debug $ "Creating process " <> T.pack name <> ": " <> T.pack (show $ cmdspec p)
90+ putLog Debug $ "Creating process " <> T.pack name <> ": " <> T.pack (reconstructCommand (cmdspec p))
8991 liftIO $ Process.createProcess p
9092
9193-- | Like `System.Process.callProcess` but also logs (debug) the process being run
@@ -130,3 +132,10 @@ streamToLog stream = fix $ \loop -> do
130132 liftIO (Streams.read stream) >>= \case
131133 Nothing -> return ()
132134 Just (sev, line) -> putLogRaw sev (decodeUtf8 line) >> loop
135+
136+reconstructCommand :: Process.CmdSpec -> String
137+reconstructCommand (Process.ShellCommand str) = str
138+reconstructCommand (Process.RawCommand c as) = processToShellString c as
139+ where
140+ processToShellString cmd args = unwords $ map quoteAndEscape (cmd : args)
141+ quoteAndEscape x = T.unpack $ "'" <> T.replace "'" "'\''" (T.pack x) <> "'"
lib/command/obelisk-command.cabal+1
@@ -24,6 +24,7 @@ library
2424 , github
2525 , here
2626 , hit
27+ , hpack
2728 , io-streams
2829 , exceptions
2930 , logging-effect
lib/command/src/Obelisk/App.hs+4−3
@@ -11,8 +11,9 @@ import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
1111import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
1212import Control.Monad.Trans.Class (lift)
1313import System.Directory (XdgDirectory (XdgData), getXdgDirectory)
14+import Control.Monad.Log (MonadLog)
1415
15−import Obelisk.CliApp (Cli, CliConfig, CliT, HasCliConfig, getCliConfig, runCli)
16+import Obelisk.CliApp (CliConfig, CliT, HasCliConfig, getCliConfig, runCli, Output)
1617
1718newtype Obelisk = Obelisk
1819 { _obelisk_cliConfig :: CliConfig
@@ -25,7 +26,7 @@ newtype ObeliskT m a = ObeliskT
2526 ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
2627 , HasObelisk, HasCliConfig)
2728
28−deriving instance Monad m => Cli (ObeliskT m)
29+deriving instance Monad m => MonadLog Output (ObeliskT m)
2930
3031class HasObelisk m where
3132 getObelisk :: m Obelisk
@@ -43,7 +44,7 @@ runObelisk c =
4344 . unObeliskT
4445
4546type MonadObelisk m =
46− ( Cli m
47+ ( MonadLog Output m
4748 , HasCliConfig m
4849 , HasObelisk m
4950 , MonadIO m
lib/command/src/Obelisk/Command.hs+3−1
@@ -16,7 +16,6 @@ import qualified Data.ByteString.Base16 as Base16
1616import qualified Data.ByteString.Lazy as LBS
1717import Data.List
1818import Data.Maybe (catMaybes, listToMaybe)
19−import Data.Monoid
2019import Data.Text (Text)
2120import qualified Data.Text as T
2221import Data.Text.Encoding
@@ -93,6 +92,7 @@ data ObCommand
9392 | ObCommand_Run
9493 | ObCommand_Thunk ThunkCommand
9594 | ObCommand_Repl
95+ | ObCommand_Watch
9696 | ObCommand_Upgrade (Maybe Text)
9797 | ObCommand_Internal ObInternal
9898 deriving Show
@@ -132,6 +132,7 @@ obCommand cfg = hsubparser
132132 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
133133 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
134134 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
135+ , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
135136 , command "upgrade" $ info (ObCommand_Upgrade <$> argument (maybeReader $ Just . Just . T.pack) (action "branch" <> metavar "GITBRANCH" <> value Nothing <> help "Git branch of obelisk to update to (defaults to the thunk's branch)")) $ progDesc "Upgrade Obelisk in the project"
136137 ])
137138 <|> subparser
@@ -407,6 +408,7 @@ ob = \case
407408 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
408409 ThunkCommand_Pack thunks -> forM_ thunks $ \(ThunkPackOpts dir upstream) -> packThunk dir (T.pack upstream)
409410 ObCommand_Repl -> runRepl
411+ ObCommand_Watch -> inNixShell' $ static runWatch
410412 ObCommand_Upgrade branch -> do
411413 upgradeObelisk "." branch
412414 ObCommand_Internal icmd -> case icmd of
lib/command/src/Obelisk/Command/Deploy.hs+2−2
@@ -13,7 +13,6 @@ import Data.Bits
1313import Data.Default
1414import qualified Data.Map as Map
1515import Data.Maybe
16−import Data.Monoid
1716import qualified Data.Text as T
1817import qualified Data.Text.IO as T
1918import System.Directory
@@ -94,7 +93,7 @@ deployPush deployPath getNixBuilders = do
9493 buildOutput <- nixBuild $ def
9594 { _nixBuildConfig_target = Target
9695 { _target_path = srcPath
97− , _target_attr = Just "server"
96+ , _target_attr = Just "server.system"
9897 }
9998 , _nixBuildConfig_outLink = OutLink_None
10099 , _nixBuildConfig_args =
@@ -102,6 +101,7 @@ deployPush deployPath getNixBuilders = do
102101 , strArg "adminEmail" adminEmail
103102 , strArg "routeHost" routeHost
104103 , boolArg "enableHttps" enableHttps
104+ , rawArg "config" $ deployPath </> "config"
105105 ]
106106 , _nixBuildConfig_builders = builders
107107 }
lib/command/src/Obelisk/Command/Nix.hs+4
@@ -11,6 +11,7 @@ module Obelisk.Command.Nix
1111 , OutLink (..)
1212 , Arg (..)
1313 , boolArg
14+ , rawArg
1415 , strArg
1516 ) where
1617
@@ -57,6 +58,9 @@ data Arg
5758strArg :: String -> String -> Arg
5859strArg k = Arg_Str k
5960
61+rawArg :: String -> String -> Arg
62+rawArg k = Arg_Expr k
63+
6064boolArg :: String -> Bool -> Arg
6165boolArg k = Arg_Expr k . bool "false" "true"
6266
lib/command/src/Obelisk/Command/Project.hs+1−2
@@ -20,7 +20,6 @@ import Control.Monad.IO.Class (liftIO)
2020import Control.Monad.State
2121import Data.Bits
2222import Data.Function (on)
23−import Data.Monoid
2423import qualified Data.Text as T
2524import System.Directory
2625import System.FilePath
@@ -111,7 +110,7 @@ initProject source = withSystemTempDirectory "ob-init" $ \tmpDir -> do
111110--TODO: Allow the user to ignore our security concerns
112111-- | Find the Obelisk implementation for the project at the given path
113112findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
114−findProjectObeliskCommand target = withSpinnerNoTrail "Locating project obelisk" $ do
113+findProjectObeliskCommand target = do
115114 myUid <- liftIO getRealUserID
116115 targetStat <- liftIO $ getFileStatus target
117116 (result, insecurePaths) <- flip runStateT [] $ walkToProjectRoot target targetStat myUid >>= \case
lib/command/src/Obelisk/Command/Run.hs+86−32
@@ -13,31 +13,45 @@ import Data.Either
1313import Data.List
1414import Data.List.NonEmpty as NE
1515import Data.Maybe
16−import Data.Monoid
1716import qualified Data.Text as T
18−import Distribution.PackageDescription.Parse (ParseResult (..), parseGenericPackageDescription)
17+import Distribution.PackageDescription.Parsec (parseGenericPackageDescription)
18+import Distribution.Parsec.ParseResult (runParseResult)
19+import Distribution.Pretty
1920import Distribution.Types.BuildInfo
2021import Distribution.Types.CondTree
2122import Distribution.Types.GenericPackageDescription
2223import Distribution.Types.Library
23−import Distribution.Utils.Generic (withUTF8FileContents)
24+import Distribution.Utils.Generic
25+import Hpack.Config
26+import Hpack.Render
27+import Hpack.Yaml
28+import Language.Haskell.Extension
2429import Network.Socket
2530import System.Directory
2631import System.FilePath
2732import System.IO.Temp (withSystemTempDirectory)
33+import Data.ByteString (ByteString)
2834
2935import Obelisk.App (MonadObelisk, ObeliskT)
3036import Obelisk.CliApp (CliT (..), HasCliConfig, Severity (..), callCommand, failWith, getCliConfig, putLog,
3137 runCli)
3238import Obelisk.Command.Project (inProjectShell)
3339
40+data CabalPackageInfo = CabalPackageInfo
41+ { _cabalPackageInfo_packageRoot :: FilePath
42+ , _cabalPackageInfo_sourceDirs :: NE.NonEmpty FilePath
43+ -- ^ List of hs src dirs of the library component
44+ , _cabalPackageInfo_defaultExtensions :: [Extension]
45+ -- ^ List of globally enable extensions of the library component
46+ }
47+
3448-- NOTE: `run` is not polymorphic like the rest because we use StaticPtr to invoke it.
3549run :: ObeliskT IO ()
3650run = do
3751 pkgs <- getLocalPkgs
3852 withGhciScript pkgs $ \dotGhciPath -> do
3953 freePort <- getFreePort
40− runGhcid dotGhciPath $ Just $ unwords ["main", show freePort]
54+ runGhcid dotGhciPath $ Just $ unwords ["run", show freePort, "Backend.backend", "Frontend.frontend"]
4155
4256runRepl :: MonadObelisk m => m ()
4357runRepl = do
@@ -45,35 +59,63 @@ runRepl = do
4559 withGhciScript pkgs $ \dotGhciPath -> do
4660 runGhciRepl dotGhciPath
4761
62+runWatch :: MonadObelisk m => m ()
63+runWatch = do
64+ pkgs <- getLocalPkgs
65+ withGhciScript pkgs $ \dotGhciPath -> runGhcid dotGhciPath Nothing
66+
4867-- | Relative paths to local packages of an obelisk project
4968-- TODO a way to query this
5069getLocalPkgs :: Applicative f => f [FilePath]
5170getLocalPkgs = pure ["backend", "common", "frontend"]
5271
53−parseHsSrcDir
72+parseCabalPackage
5473 :: (MonadObelisk m)
55− => FilePath -- ^ package cabal file path
56− -> m (Maybe (NE.NonEmpty FilePath)) -- ^ List of hs src dirs of the library component
57−parseHsSrcDir cabalFp = do
58− exists <- liftIO $ doesFileExist cabalFp
59− if exists
60− then do
61− withUTF8FileContentsM cabalFp $ \cabal -> do
62− case parseGenericPackageDescription cabal of
63− ParseOk warnings gpkg -> do
64− mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
65− return $ do
66− (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
67− pure $ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
68− ParseFailed e -> do
69− putLog Error $ T.pack $ "Failed to parse cabal file: " <> show e
74+ => FilePath -- ^ package directory
75+ -> m (Maybe CabalPackageInfo)
76+parseCabalPackage dir = do
77+ let cabalFp = dir </> (takeBaseName dir <> ".cabal")
78+ hpackFp = dir </> "package.yaml"
79+ hasCabal <- liftIO $ doesFileExist cabalFp
80+ hasHpack <- liftIO $ doesFileExist hpackFp
81+
82+ mCabalContents <- if hasCabal
83+ then Just <$> liftIO (readUTF8File cabalFp)
84+ else if hasHpack
85+ then do
86+ let decodeOptions = DecodeOptions hpackFp Nothing decodeYaml
87+ liftIO (readPackageConfig decodeOptions) >>= \case
88+ Left err -> do
89+ putLog Error $ T.pack $ "Failed to parse " <> hpackFp <> ": " <> err
7090 return Nothing
71− else return Nothing
91+ Right (DecodeResult hpackPackage _ _) -> do
92+ return $ Just $ renderPackage [] hpackPackage
93+ else return Nothing
7294
73−withUTF8FileContentsM :: (MonadIO m, HasCliConfig m) => FilePath -> (String -> CliT IO a) -> m a
95+ fmap join $ forM mCabalContents $ \cabalContents -> do
96+ let (warnings, result) = runParseResult $ parseGenericPackageDescription $
97+ toUTF8BS $ cabalContents
98+ mapM_ (putLog Warning) $ fmap (T.pack . show) warnings
99+ case result of
100+ Right gpkg -> do
101+ return $ do
102+ (_, lib) <- simplifyCondTree (const $ pure True) <$> condLibrary gpkg
103+ pure $ CabalPackageInfo
104+ { _cabalPackageInfo_packageRoot = takeDirectory cabalFp
105+ , _cabalPackageInfo_sourceDirs =
106+ fromMaybe (pure ".") $ NE.nonEmpty $ hsSourceDirs $ libBuildInfo lib
107+ , _cabalPackageInfo_defaultExtensions =
108+ defaultExtensions $ libBuildInfo lib
109+ }
110+ Left (_, errors) -> do
111+ putLog Error $ T.pack $ "Failed to parse " <> cabalFp <> ":"
112+ mapM_ (putLog Error) $ fmap (T.pack . show) errors
113+ return Nothing
114+
115+withUTF8FileContentsM :: (MonadIO m, HasCliConfig m) => FilePath -> (ByteString -> CliT IO a) -> m a
74116withUTF8FileContentsM fp f = do
75117 c <- getCliConfig
76− liftIO $ withUTF8FileContents fp $ runCli c . f
118+ liftIO $ withUTF8FileContents fp $ runCli c . f . toUTF8BS
77119
78120-- | Create ghci configuration to load the given packages
79121withGhciScript
@@ -82,26 +124,37 @@ withGhciScript
82124 -> (FilePath -> m ()) -- ^ Action to run with the path to generated temporory .ghci
83125 -> m ()
84126withGhciScript pkgs f = do
85− (pkgDirErrs, hsSrcDirs) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
86− let cabalFp = pkg </> pkg <.> "cabal"
87− flip fmap (parseHsSrcDir cabalFp) $ \case
127+ (pkgDirErrs, packageInfos) <- fmap partitionEithers $ forM pkgs $ \pkg -> do
128+ flip fmap (parseCabalPackage pkg) $ \case
88129 Nothing -> Left pkg
89− Just hsSrcDirs -> Right $ toList $ fmap (pkg </>) hsSrcDirs
130+ Just packageInfo -> Right packageInfo
90131
91− when (null hsSrcDirs) $
132+ when (null packageInfos) $
92133 failWith $ T.pack $ "No valid pkgs found in " <> intercalate ", " pkgs
93134 unless (null pkgDirErrs) $
94135 putLog Warning $ T.pack $ "Failed to find pkgs in " <> intercalate ", " pkgDirErrs
95136
96− let dotGhci = unlines
97− [ ":set -i" <> intercalate ":" (mconcat hsSrcDirs)
98− , ":load ./devel/Devel.hs" -- TODO more robust filepath
137+ let extensions = packageInfos >>= _cabalPackageInfo_defaultExtensions
138+ extensionsLine = if extensions == mempty
139+ then ""
140+ else ":set " <> intercalate " " ((("-X" <>) . prettyShow) <$> extensions)
141+ dotGhci = unlines $
142+ [ ":set -i" <> intercalate ":" (packageInfos >>= rootedSourceDirs)
143+ , extensionsLine
144+ , ":load Backend Frontend"
145+ , "import Obelisk.Run"
146+ , "import qualified Frontend"
147+ , "import qualified Backend"
99148 ]
100149 withSystemTempDirectory "ob-ghci" $ \fp -> do
101150 let dotGhciPath = fp </> ".ghci"
102151 liftIO $ writeFile dotGhciPath dotGhci
103152 f dotGhciPath
104153
154+ where
155+ rootedSourceDirs pkg = NE.toList $
156+ (_cabalPackageInfo_packageRoot pkg </>) <$> _cabalPackageInfo_sourceDirs pkg
157+
105158-- | Run ghci repl
106159runGhciRepl
107160 :: MonadObelisk m
@@ -119,7 +172,8 @@ runGhcid dotGhci mcmd = callCommand $ unwords $ "ghcid" : opts
119172 where
120173 opts =
121174 [ "-W"
122− , "--command='ghci -Wall -ghci-script " <> dotGhci <> "' "
175+ --TODO: The decision of whether to use -fwarn-redundant-constraints should probably be made by the user
176+ , "--command='ghci -Wall -ignore-dot-ghci -fwarn-redundant-constraints -ghci-script " <> dotGhci <> "' "
123177 , "--reload=config"
124178 , "--outputfile=ghcid-output.txt"
125179 ] <> testCmd
lib/command/src/Obelisk/Command/Thunk.hs−1
@@ -50,7 +50,6 @@ import Data.List.NonEmpty (NonEmpty (..))
5050import qualified Data.List.NonEmpty as NonEmpty
5151import qualified Data.Map as Map
5252import Data.Maybe
53−import Data.Monoid
5453import Data.Set (Set)
5554import qualified Data.Set as Set
5655import Data.Text (Text)
lib/command/src/Obelisk/Command/Upgrade.hs+1−1
@@ -33,7 +33,7 @@ import Obelisk.Command.Upgrade.Hash
3333import Obelisk.Migration
3434
3535newtype HandOffAction = HandoffAction Any
36− deriving (Monoid, Ord, Eq, Show)
36+ deriving (Monoid, Semigroup, Ord, Eq, Show)
3737
3838instance Action HandOffAction where
3939 parseEdgeMeta = HandoffAction . Any . (== "True")
lib/executable-config/README.md+2−7
@@ -7,10 +7,6 @@ Obelisk.ExecutableConfig serves two related purposes:
771. Injection: a cross-platform injection function, which can be used to put configuration files in a canonical location.
882. Retrieval: a cross-platform retrieval function that is aware of the canonical location on each platform and how to read files from that location.
99
10−## File Location
11−
12−ExecutableConfig expects your configuration files to be placed in `./config`.
13−
1410## Supported Platforms
1511
1612| Function | iOS | Android | Warp |Web backend + GHCJS |
@@ -28,7 +24,7 @@ ExecutableConfig expects your configuration files to be placed in `./config`.
2824
2925There is currently no `inject` function for `jsaddle-warp`-based frontends.
3026
31−`get` is a Haskell function that simply reads the configuration files from `./config`.
27+`get` is a Haskell function that simply reads the configuration files relative to the current directory.
3228
3329### Web backend + GHCJS
3430
@@ -36,9 +32,8 @@ There is currently no `inject` function for `jsaddle-warp`-based frontends.
3632
3733`inject` is a Haskell function that produces a snippet of HTML that must be appended to the `<head>` of the page that the backend serves (i.e., the entry point for the GHCJS application).
3834
39−`get` is a Haskell function that reads the configuration files from `./config`.
35+`get` is a Haskell function that reads the configuration files relative to the current directory.
4036
4137#### GHCJS
4238
4339`get` is a Haskell function that can be run by the GHCJS frontend to retrieve the injected configuration data from the served `<head>`. There is no `inject` that is run in GHCJS.
44−
lib/executable-config/default.nix+2−1
@@ -7,7 +7,8 @@
77}:
88let injectConfig = config: assets: nixpkgs.runCommand "inject-config" {} ''
99 set -x
10− cp -a "${assets}" $out
10+ mkdir -p $out
11+ cp --no-preserve=mode -Lr "${assets}" $out/static
1112 chmod +w "$out"
1213 if ! mkdir $out/config; then
1314 2>&1 echo config directory already exists or could not be created
lib/executable-config/inject/obelisk-executable-config-inject.cabal+2
@@ -9,8 +9,10 @@ library
99 build-depends:
1010 base,
1111 bytestring,
12+ directory,
1213 filepath,
1314 reflex-dom-core,
1415 text
1516
1617 exposed-modules: Obelisk.ExecutableConfig.Inject
18+ ghc-options: -Wall
lib/executable-config/inject/src/Obelisk/ExecutableConfig/Inject.hs+40−7
@@ -1,18 +1,51 @@
11{-# LANGUAGE OverloadedStrings #-}
22module Obelisk.ExecutableConfig.Inject where
33
4+import Control.Monad (mapM_)
5+import Control.Monad.IO.Class (MonadIO, liftIO)
46import Data.Semigroup ((<>))
57import Data.ByteString (ByteString)
8+import Data.Text (Text)
69import qualified Data.Text as T
710import qualified Data.Text.IO as T
8−import Reflex.Dom.Core
11+import Reflex.Dom.Core hiding (value)
12+import System.Directory
913import System.FilePath ((</>))
1014
15+-- | Produces a @<script>@ tag containing the contents of the config item,
16+-- rendered as a 'ByteString'.
1117inject :: FilePath -> IO ByteString
12−inject item = do
13− f <- T.readFile $ "config" </> item
14− fmap snd $ renderStatic $ injectPure item f
18+inject key = do
19+ value <- T.readFile key
20+ fmap snd $ renderStatic $ injectPure (T.pack key) value
1521
16−injectPure :: DomBuilder t m => FilePath -> T.Text -> m ()
17−injectPure item content = elAttr "script" ("type" =: "text/plain" <> "id" =: ("config-" <> T.pack item)) $
18− text content
22+-- | 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 ()
25+injectPure 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)
37+
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 []
lib/executable-config/lookup/src-android/Obelisk/ExecutableConfig.hs+1−1
@@ -15,7 +15,7 @@ get :: Text -> IO (Maybe Text)
1515get name = bracket getAssets freeAssetManager $ \mgrObj -> do
1616 mgr <- assetManagerFromJava mgrObj
1717 let open = do
18− a <- withCString ("config" </> T.unpack name) $ \fn ->
18+ a <- withCString (T.unpack name) $ \fn ->
1919 assetManager_open mgr fn 3
2020 return $ if unAAsset a == nullPtr
2121 then Nothing
lib/executable-config/lookup/src-ios/Obelisk/ExecutableConfig.hs+1−1
@@ -14,7 +14,7 @@ import System.IO.Error
1414
1515get :: Text -> IO (Maybe Text)
1616get name = fmap join $ mainBundleResourcePath >>= \mp -> forM mp $ \p ->
17− catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> "config" </> T.unpack name
17+ catchDoesNotExist $ T.readFile $ T.unpack (T.decodeUtf8 p) </> T.unpack name
1818
1919catchDoesNotExist :: IO a -> IO (Maybe a)
2020catchDoesNotExist f = catchJust doesNotExist (Just <$> f) $ const $ return Nothing
lib/executable-config/lookup/src/Obelisk/ExecutableConfig.hs+1−1
@@ -10,4 +10,4 @@ import System.IO.Error
1010get :: Text -> IO (Maybe Text)
1111get path = do
1212 let doesNotExist = \e -> if isDoesNotExistError e then Just () else Nothing
13− catchJust doesNotExist (fmap Just $ T.readFile $ "config" </> T.unpack path) (\_ -> pure Nothing)
13+ catchJust doesNotExist (fmap Just $ T.readFile $ T.unpack path) (\_ -> pure Nothing)
lib/frontend/obelisk-frontend.cabalnew file+22
@@ -0,0 +1,22 @@
1+name: obelisk-frontend
2+version: 0.1
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ bytestring,
10+ dependent-sum,
11+ ghcjs-dom,
12+ jsaddle,
13+ lens,
14+ obelisk-route,
15+ primitive,
16+ ref-tf,
17+ reflex,
18+ reflex-dom-core,
19+ text,
20+ transformers
21+ exposed-modules: Obelisk.Frontend
22+ ghc-options: -Wall
lib/frontend/src/Obelisk/Frontend.hsnew file+165
@@ -0,0 +1,165 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE FlexibleInstances #-}
4+{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE MultiParamTypeClasses #-}
6+{-# LANGUAGE OverloadedStrings #-}
7+{-# LANGUAGE RankNTypes #-}
8+{-# LANGUAGE ScopedTypeVariables #-}
9+{-# LANGUAGE TemplateHaskell #-}
10+{-# LANGUAGE TypeFamilies #-}
11+{-# LANGUAGE UndecidableInstances #-}
12+module Obelisk.Frontend
13+ ( ObeliskWidget
14+ , Frontend (..)
15+ , runFrontend
16+ , renderFrontendHtml
17+ ) where
18+
19+import Prelude hiding ((.))
20+
21+import Control.Category
22+import Control.Concurrent
23+import Control.Lens
24+import Control.Monad
25+import Control.Monad.Fix
26+import Control.Monad.IO.Class
27+import Control.Monad.Primitive
28+import Control.Monad.Ref
29+import Control.Monad.Trans.Class
30+import Data.ByteString (ByteString)
31+import Data.Dependent.Sum (DSum (..))
32+import Data.Functor.Sum
33+import Data.IORef
34+import Data.Monoid hiding (Sum)
35+import GHCJS.DOM hiding (bracket, catch)
36+import GHCJS.DOM.Document
37+import GHCJS.DOM.Node
38+import qualified GHCJS.DOM.Types as DOM
39+import Language.Javascript.JSaddle (JSM)
40+import Obelisk.Route.Frontend
41+import Reflex.Dom.Core
42+import Reflex.Host.Class
43+import qualified Reflex.TriggerEvent.Base as TriggerEvent
44+
45+makePrisms ''Sum
46+
47+type ObeliskWidget t x route m =
48+ ( DomBuilder t m
49+ , MonadFix m
50+ , MonadHold t m
51+ , MonadSample t (Performable m)
52+ , MonadReflexCreateTrigger t m
53+ , PostBuild t m
54+ , PerformEvent t m
55+ , MonadIO m
56+ , MonadIO (Performable m)
57+ , TriggerEvent t m
58+ , HasDocument m
59+ , MonadRef m
60+ , Ref m ~ Ref IO
61+ , MonadRef (Performable m)
62+ , Ref (Performable m) ~ Ref IO
63+ , MonadFix (Performable m)
64+ , PrimMonad m
65+ , Prerender x m
66+ , SetRoute t route m
67+ )
68+
69+data Frontend route = Frontend
70+ { _frontend_head :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
71+ , _frontend_body :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
72+ }
73+
74+type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
75+
76+-- | A widget that isn't attached to any particular part of the DOM hierarchy
77+type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
78+
79+type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
80+
81+--TODO: Rename
82+{-# INLINABLE attachWidget''' #-}
83+attachWidget'''
84+ :: (EventChannel -> PerformEventT DomTimeline (SpiderHost Global) (IORef (Maybe (EventTrigger DomTimeline ()))))
85+ -> IO ()
86+attachWidget''' w = runDomHost $ do
87+ events <- liftIO newChan
88+ (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
89+ mPostBuildTrigger <- readRef postBuildTriggerRef
90+ forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
91+ liftIO $ processAsyncEvents events fc
92+
93+--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
94+runWithHeadAndBody
95+ :: ( (forall c. Widget' () c -> FloatingWidget () c) -- "Append to head"
96+ -> (forall c. Widget' () c -> FloatingWidget () c) -- "Append to body"
97+ -> FloatingWidget () ()
98+ )
99+ -> JSM ()
100+runWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
101+ globalDoc <- currentDocumentUnchecked
102+ headFragment <- createDocumentFragment globalDoc
103+ bodyFragment <- createDocumentFragment globalDoc
104+ unreadyChildren <- liftIO $ newIORef 0
105+ let commit = do
106+ headElement <- getHeadUnchecked globalDoc
107+ bodyElement <- getBodyUnchecked globalDoc
108+ void $ inAnimationFrame' $ \_ -> do
109+ replaceElementContents headElement headFragment
110+ replaceElementContents bodyElement bodyFragment
111+ liftIO $ attachWidget''' $ \events -> flip runWithJSContextSingleton jsSing $ do
112+ (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
113+ let appendImmediateDom :: DOM.DocumentFragment -> Widget' () c -> FloatingWidget () c
114+ appendImmediateDom df w = do
115+ events' <- TriggerEvent.askEvents
116+ lift $ do
117+ doc <- getOwnerDocumentUnchecked df
118+ let builderEnv = ImmediateDomBuilderEnv
119+ { _immediateDomBuilderEnv_document = doc
120+ , _immediateDomBuilderEnv_parent = toNode df
121+ , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
122+ , _immediateDomBuilderEnv_commitAction = commit
123+ }
124+ runImmediateDomBuilderT w builderEnv events'
125+ flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (appendImmediateDom headFragment) (appendImmediateDom bodyFragment)
126+ liftIO (readIORef unreadyChildren) >>= \case
127+ 0 -> DOM.liftJSM commit
128+ _ -> return ()
129+ return postBuildTriggerRef
130+
131+runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
132+runFrontend validFullEncoder frontend = do
133+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
134+ errorLeft = \case
135+ Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
136+ Right x -> Identity x
137+ runMyRouteViewT
138+ :: ( TriggerEvent t m
139+ , PerformEvent t m
140+ , MonadHold t m
141+ , DOM.MonadJSM m
142+ , DOM.MonadJSM (Performable m)
143+ , MonadFix m
144+ , MonadFix (Performable m)
145+ )
146+ => RoutedT t (R route) (SetRouteT t (R route) m) a
147+ -> m a
148+ runMyRouteViewT = runRouteViewT ve
149+ runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
150+ mapRoutedT (mapSetRouteT appendHead) $ _frontend_head frontend
151+ mapRoutedT (mapSetRouteT appendBody) $ _frontend_body frontend
152+
153+renderFrontendHtml
154+ :: (t ~ SpiderTimeline Global)
155+ => r
156+ -> RoutedT t r (SetRouteT t r' (PostBuildT Spider (StaticDomBuilderT Spider (PerformEventT Spider (SpiderHost Global))))) ()
157+ -> RoutedT t r (SetRouteT t r' (PostBuildT Spider (StaticDomBuilderT Spider (PerformEventT Spider (SpiderHost Global))))) ()
158+ -> IO ByteString
159+renderFrontendHtml route headWidget bodyWidget = do
160+ --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
161+ html <- fmap snd $ renderStatic $ fmap fst $ runSetRouteT $ flip runRoutedT (pure route) $
162+ el "html" $ do
163+ el "head" headWidget
164+ el "body" bodyWidget
165+ return $ "<!DOCTYPE html>" <> html
lib/route/obelisk-route.cabalnew file+36
@@ -0,0 +1,36 @@
1+name: obelisk-route
2+version: 0.2
3+cabal-version: >= 1.2
4+build-type: Simple
5+
6+library
7+ hs-source-dirs: src
8+ build-depends: base,
9+ categories,
10+ constraints,
11+ containers,
12+ dependent-map,
13+ dependent-sum,
14+ dependent-sum-universe-orphans,
15+ dependent-sum-template,
16+ either,
17+ ghcjs-dom,
18+ http-types,
19+ jsaddle,
20+ lens,
21+ monad-control,
22+ mtl,
23+ network-uri,
24+ primitive,
25+ ref-tf,
26+ reflex,
27+ reflex-dom-core,
28+ template-haskell,
29+ text,
30+ transformers,
31+ universe,
32+ universe-template
33+ exposed-modules: Obelisk.Route
34+ Obelisk.Route.TH
35+ Obelisk.Route.Frontend
36+ ghc-options: -Wall -fwarn-redundant-constraints
lib/route/src/Obelisk/Route.hsnew file+795
@@ -0,0 +1,795 @@
1+{-# LANGUAGE ApplicativeDo #-}
2+{-# LANGUAGE ConstraintKinds #-}
3+{-# LANGUAGE EmptyCase #-}
4+{-# LANGUAGE EmptyDataDecls #-}
5+{-# LANGUAGE FlexibleContexts #-}
6+{-# LANGUAGE FlexibleInstances #-}
7+{-# LANGUAGE GADTs #-}
8+{-# LANGUAGE KindSignatures #-}
9+{-# LANGUAGE LambdaCase #-}
10+{-# LANGUAGE MultiParamTypeClasses #-}
11+{-# LANGUAGE OverloadedStrings #-}
12+{-# LANGUAGE PatternSynonyms #-}
13+{-# LANGUAGE RankNTypes #-}
14+{-# LANGUAGE ScopedTypeVariables #-}
15+{-# LANGUAGE TemplateHaskell #-}
16+{-# LANGUAGE TypeFamilies #-}
17+{-# LANGUAGE TypeOperators #-}
18+{-# LANGUAGE ViewPatterns #-}
19+module Obelisk.Route
20+ ( R
21+ , pattern (:/)
22+ , PageName
23+ , PathQuery
24+ , Encoder
25+ , unsafeEncoder
26+ , checkEncoder
27+ , EncoderImpl (..)
28+ , EncoderFunc (..)
29+ , unsafeMkEncoder
30+ , encode
31+ , decode
32+ , tryDecode
33+ , hoistCheck
34+ , hoistParse
35+ , mapSome
36+ , SegmentResult (..)
37+ , pathComponentEncoder
38+ , enumEncoder
39+ , enum1Encoder
40+ , checkEnum1EncoderFunc
41+ , unitEncoder
42+ , pathOnlyEncoder
43+ , singletonListEncoder
44+ , unpackTextEncoder
45+ , prefixTextEncoder
46+ , unsafeTshowEncoder
47+ , someConstEncoder
48+ , singlePathSegmentEncoder
49+ , justEncoder
50+ , nothingEncoder
51+ , isoEncoder
52+ , wrappedEncoder
53+ , unwrappedEncoder
54+ , listToNonEmptyEncoder
55+ , prefixNonemptyTextEncoder
56+ , joinPairTextEncoder
57+ , toListMapEncoder
58+ , shadowEncoder
59+ , prismEncoder
60+ , rPrism
61+ , obeliskRouteEncoder
62+ , obeliskRouteSegment
63+ , pageNameEncoder
64+ , handleEncoder
65+ , ObeliskRoute (..)
66+ , _ObeliskRoute_App
67+ , _ObeliskRoute_Resource
68+ , ResourceRoute (..)
69+ , JSaddleWarpRoute (..)
70+ , jsaddleWarpRouteEncoder
71+ , IndexOnlyRoute (..)
72+ , indexOnlyRouteSegment
73+ , indexOnlyRouteEncoder
74+ , someSumEncoder
75+ , Void1
76+ , void1Encoder
77+ , pathSegmentsTextEncoder
78+ , queryParametersTextEncoder
79+ ) where
80+
81+import Prelude hiding ((.), id)
82+
83+import Control.Applicative
84+import Control.Category (Category (..))
85+import qualified Control.Categorical.Functor as Cat
86+import Control.Categorical.Bifunctor
87+import Control.Category.Associative
88+import Control.Category.Monoidal
89+import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
90+import Control.Monad.Except
91+import Data.Dependent.Sum (DSum (..))
92+import Data.Dependent.Map (DMap)
93+import qualified Data.Dependent.Map as DMap
94+import Data.Either.Validation (Validation (..))
95+import Data.Foldable
96+import Data.Functor.Sum
97+import Data.GADT.Compare
98+import Data.GADT.Compare.TH
99+import Data.GADT.Show
100+import Data.List.NonEmpty (NonEmpty (..))
101+import Data.Map (Map)
102+import qualified Data.Map as Map
103+import Data.Maybe
104+import Data.Set (Set)
105+import qualified Data.Set as Set
106+import Data.Some (Some)
107+import qualified Data.Some as Some
108+import Data.Some.Universe.Orphans ()
109+import Data.Text (Text)
110+import qualified Data.Text as T
111+import Data.Text.Encoding
112+import Data.Universe
113+import Network.HTTP.Types.URI
114+import Obelisk.Route.TH
115+import Text.Read (readMaybe)
116+
117+-- Design goals:
118+-- No start-up time on the frontend (not yet met)
119+-- Able to ensure that there aren't overlapping routes prior to deployment
120+-- Easy to write common types of parsers
121+-- Completeness checking
122+
123+-- Laws:
124+-- We statically know that all valid Routes can be turned into valid URIs
125+-- - This means we need to know whether there are any overlaps, e.g. with the static file namespace
126+
127+--TODO:
128+-- Backend:
129+-- Redirect the user to a canonical route
130+-- Pre-render
131+-- Frontend:
132+-- Intercept links that can be kept within the app
133+-- Fragments
134+
135+--------------------------------------------------------------------------------
136+-- Subroutes/paths
137+--------------------------------------------------------------------------------
138+
139+type R f = DSum f Identity --TODO: Better name
140+
141+--TODO: COMPLETE pragma
142+infixr 5 :/
143+pattern (:/) :: f a -> a -> R f
144+pattern a :/ b = a :=> Identity b
145+
146+mapSome :: (forall a. f a -> g a) -> Some f -> Some g
147+mapSome f (Some.This a) = Some.This $ f a
148+
149+--------------------------------------------------------------------------------
150+-- Encoder fundamentals
151+--------------------------------------------------------------------------------
152+
153+-- | This is the type of route encoder/decoders. It is parameterised over two monads: Firstly, the monad
154+-- used to check the validity of the encoder (i.e. that it is total), secondly the monad used for parsing
155+-- during the decode phase. The following two parameters are respectively the type of decoded data, and the
156+-- encoded type.
157+newtype Encoder check parse decoded encoded =
158+ Encoder { unEncoder :: check (EncoderImpl parse decoded encoded) }
159+
160+unsafeEncoder :: check (EncoderImpl parse decoded encoded) -> Encoder check parse decoded encoded
161+unsafeEncoder = Encoder
162+
163+-- | The internal type used to construct primitive 'Encoder' values.
164+-- Law:
165+-- forall p. _encoderImpl_decode ve . _encoderImpl_encode ve p == pure
166+-- Note that the reverse may not be the case: when parsing, a route may be canonicalized, and erroneous routes may be collapsed to a single 404 route. However, as a consequence of the law, encode . decode must be idempotent.
167+data EncoderImpl parse decoded encoded = EncoderImpl
168+ { _encoderImpl_decode :: !(encoded -> parse decoded) -- Can fail; can lose information; must always succeed on outputs of `_encoderImpl_encode` and result in the original value
169+ , _encoderImpl_encode :: !(decoded -> encoded) -- Must be injective
170+ }
171+
172+-- | Once an 'Encoder' has been checked, so that its check monad has become 'Identity', and its parser is total
173+-- so that its parse monad is also 'Identity', it may be used to actually decode by applying this function.
174+decode :: Encoder Identity Identity decoded encoded -> encoded -> decoded
175+decode e x = runIdentity (tryDecode e x)
176+
177+-- | Once an 'Encoder' has been checked, so that its check monad has become 'Identity', even if the same is not true of the
178+-- parse monad, we may still attempt to decode with it in its parse monad.
179+tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
180+tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
181+
182+-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
183+-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
184+-- one should usually be applying decode and encode to the same 'Encoder'
185+encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
186+encode (Encoder (Identity impl)) x = _encoderImpl_encode impl x
187+
188+-- | This is a primitive used to build encoders which can't fail to check. It should not be used unless one is
189+-- reasonably certain that the law given for 'EncoderImpl' above holds.
190+unsafeMkEncoder :: (Applicative check) => EncoderImpl parse decoded encoded -> Encoder check parse decoded encoded
191+unsafeMkEncoder impl = Encoder (pure impl)
192+
193+-- | Transform the check monad of an 'Encoder' by applying a natural transformation.
194+hoistCheck :: (forall t. check t -> check' t) -> Encoder check parse a b -> Encoder check' parse a b
195+hoistCheck f (Encoder x) = Encoder (f x)
196+
197+-- | Transform the parse monad of an 'Encoder' by applying a natural transformation.
198+hoistParse :: (Functor check)
199+ => (forall t. parse t -> parse' t) -> Encoder check parse a b -> Encoder check parse' a b
200+hoistParse f (Encoder x) = Encoder (fmap (\(EncoderImpl dec enc) -> EncoderImpl (f . dec) enc) x)
201+
202+-- | Check an 'Encoder', transforming it into one whose check monad is anything we want (usually Identity).
203+checkEncoder :: (Applicative check', Functor check)
204+ => Encoder check parse decoded encoded
205+ -> check (Encoder check' parse decoded encoded)
206+checkEncoder = fmap unsafeMkEncoder . unEncoder
207+
208+instance (Applicative check, Monad parse) => Category (Encoder check parse) where
209+ id = Encoder $ pure id
210+ Encoder f . Encoder g = Encoder $ liftA2 (.) f g
211+
212+instance Monad parse => Category (EncoderImpl parse) where
213+ id = EncoderImpl
214+ { _encoderImpl_decode = pure
215+ , _encoderImpl_encode = id
216+ }
217+ f . g = EncoderImpl
218+ { _encoderImpl_decode = _encoderImpl_decode g <=< _encoderImpl_decode f
219+ , _encoderImpl_encode = _encoderImpl_encode f . _encoderImpl_encode g
220+ }
221+
222+instance Monad parse => PFunctor (,) (EncoderImpl parse) (EncoderImpl parse) where
223+ first f = bimap f id
224+instance Monad parse => QFunctor (,) (EncoderImpl parse) (EncoderImpl parse) where
225+ second g = bimap id g
226+instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (EncoderImpl parse) where
227+ bimap f g = EncoderImpl
228+ { _encoderImpl_encode = bimap (_encoderImpl_encode f) (_encoderImpl_encode g)
229+ , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
230+ }
231+
232+instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
233+ first f = bimap f id
234+instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
235+ second g = bimap id g
236+instance (Applicative check, Monad parse) => Bifunctor (,) (Encoder check parse) (Encoder check parse) (Encoder check parse) where
237+ bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
238+
239+instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (EncoderImpl parse) where
240+ fmap ve = EncoderImpl
241+ { _encoderImpl_encode = fmap $ _encoderImpl_encode ve
242+ , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
243+ }
244+
245+instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
246+ fmap e = Encoder $ do
247+ ve <- unEncoder e
248+ pure $ Cat.fmap ve
249+
250+instance Monad parse => Associative (EncoderImpl parse) (,) where
251+ associate = EncoderImpl
252+ { _encoderImpl_encode = associate
253+ , _encoderImpl_decode = pure . disassociate
254+ }
255+ disassociate = EncoderImpl
256+ { _encoderImpl_encode = disassociate
257+ , _encoderImpl_decode = pure . associate
258+ }
259+
260+instance Monad parse => Monoidal (EncoderImpl parse) (,) where
261+ type Id (EncoderImpl parse) (,) = ()
262+ idl = EncoderImpl
263+ { _encoderImpl_encode = idl
264+ , _encoderImpl_decode = pure . coidl
265+ }
266+ idr = EncoderImpl
267+ { _encoderImpl_encode = idr
268+ , _encoderImpl_decode = pure . coidr
269+ }
270+ coidl = EncoderImpl
271+ { _encoderImpl_encode = coidl
272+ , _encoderImpl_decode = pure . idl
273+ }
274+ coidr = EncoderImpl
275+ { _encoderImpl_encode = coidr
276+ , _encoderImpl_decode = pure . idr
277+ }
278+
279+instance (Applicative check, Monad parse) => Associative (Encoder check parse) (,) where
280+ associate = Encoder $ pure associate
281+ disassociate = Encoder $ pure disassociate
282+
283+instance (Applicative check, Monad parse) => Monoidal (Encoder check parse) (,) where
284+ type Id (Encoder check parse) (,) = ()
285+ idl = Encoder $ pure idl
286+ idr = Encoder $ pure idr
287+ coidl = Encoder $ pure coidl
288+ coidr = Encoder $ pure coidr
289+
290+--------------------------------------------------------------------------------
291+-- Specific instances of encoders
292+--------------------------------------------------------------------------------
293+
294+-- | Given a valid 'Iso' from lens, construct an 'Encoder'
295+isoEncoder :: (Applicative check, Applicative parse) => Iso' a b -> Encoder check parse a b
296+isoEncoder f = unsafeMkEncoder $ EncoderImpl
297+ { _encoderImpl_encode = view f
298+ , _encoderImpl_decode = pure . view (from f)
299+ }
300+
301+wrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse (Unwrapped a) a
302+wrappedEncoder = isoEncoder $ from _Wrapped'
303+
304+unwrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse a (Unwrapped a)
305+unwrappedEncoder = isoEncoder _Wrapped'
306+
307+-- | Encodea a value by simply applying 'Just'
308+justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
309+justEncoder = prismEncoder _Just
310+
311+-- |
312+nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
313+nothingEncoder = prismEncoder _Nothing
314+
315+someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
316+someConstEncoder = unsafeMkEncoder $ EncoderImpl
317+ { _encoderImpl_encode = \(Some.This (Const a)) -> a
318+ , _encoderImpl_decode = pure . Some.This . Const
319+ }
320+
321+-- | WARNING: This is only safe if the Show and Read instances for 'a' are
322+-- inverses of each other
323+unsafeTshowEncoder :: (Show a, Read a, Applicative check, MonadError Text parse) => Encoder check parse a Text
324+unsafeTshowEncoder = unsafeMkEncoder $ EncoderImpl
325+ { _encoderImpl_encode = tshow
326+ , _encoderImpl_decode = \raw -> case readMaybe $ T.unpack raw of
327+ Nothing -> throwError $ "unsafeTshowEncoderImpl: couldn't decode " <> tshow raw
328+ Just parsed -> pure parsed
329+ }
330+
331+newtype EncoderFunc check parse p r = EncoderFunc { runEncoderImplFunc :: forall a. p a -> Encoder check parse a r }
332+
333+newtype Flip f a b = Flip { unFlip :: f b a }
334+
335+checkEnum1EncoderFunc
336+ :: forall check check' parse p r.
337+ ( Universe (Some p)
338+ , GCompare p
339+ , Monad check
340+ , Applicative check'
341+ )
342+ => (forall a. p a -> Encoder check parse a r)
343+ -> check (EncoderFunc check' parse p r)
344+checkEnum1EncoderFunc f = do
345+ (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
346+ traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
347+ pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
348+ DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
349+
350+-- | This type is used by pathComponentEncoder to allow the user to indicate how to treat various cases when encoding a dependent sum of type `(R p)`.
351+data SegmentResult check parse a =
352+ PathEnd (Encoder check parse a (Map Text (Maybe Text))) -- ^ Indicate that the path is finished, with an Encoder that translates the corresponding value into query parameters
353+ | PathSegment Text (Encoder check parse a PageName) -- ^ Indicate that the key should be represented by an additional path segment with the given 'Text', and give an Encoder for translating the corresponding value into the remainder of the route.
354+
355+-- | Encode a dependent sum of type `(R p)` into a PageName (i.e. the path and query part of a URL) by using the
356+-- supplied function to decide how to encode the constructors of p using the SegmentResult type. It is important
357+-- that the number of values of type `(Some p)` be relatively small in order for checking to complete quickly.
358+pathComponentEncoder
359+ :: forall check parse p.
360+ ( Universe (Some p)
361+ , GShow p
362+ , GCompare p
363+ , MonadError Text check
364+ , MonadError Text parse )
365+ => (forall a. p a -> SegmentResult check parse a)
366+ -> Encoder check parse (R p) PageName
367+pathComponentEncoder f = Encoder $ do
368+ let extractEncoder = \case
369+ PathEnd e -> first (unitEncoder []) . coidl . e
370+ PathSegment _ e -> e
371+ extractPathSegment = \case
372+ PathEnd _ -> Nothing
373+ PathSegment t _ -> Just t
374+ EncoderFunc f' <- checkEnum1EncoderFunc (extractEncoder . f)
375+ unEncoder (pathComponentEncoderImpl (enum1Encoder (extractPathSegment . f)) f')
376+
377+pathComponentEncoderImpl :: forall check parse p. (Monad check, Monad parse)
378+ => (Encoder check parse (Some p) (Maybe Text))
379+ -> (forall a. p a -> Encoder Identity parse a PageName)
380+ -> Encoder check parse (R p) PageName
381+pathComponentEncoderImpl this rest =
382+ chainEncoder (lensEncoder (\(_, b) a -> (a, b)) Prelude.fst consEncoder) this rest
383+
384+--NOTE: Naming convention in this module is to always talk about things in the *encoding* direction, never in the *decoding* direction
385+
386+chainEncoder
387+ :: forall check parse p r b.
388+ ( Monad check
389+ , Monad parse
390+ )
391+ => Encoder check parse (b, r) r
392+ -> Encoder check parse (Some p) b
393+ -> (forall a. p a -> Encoder Identity parse a r)
394+ -> Encoder check parse (R p) r
395+chainEncoder cons this rest = Encoder $ do
396+ consValid <- unEncoder cons
397+ thisValid <- unEncoder this
398+ pure $ EncoderImpl
399+ { _encoderImpl_decode = \v -> do
400+ (here, following) <- _encoderImpl_decode consValid v
401+ Some.This r <- _encoderImpl_decode thisValid here
402+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
403+ , _encoderImpl_encode = \(r :/ s) ->
404+ _encoderImpl_encode consValid
405+ ( _encoderImpl_encode thisValid $ Some.This r
406+ , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
407+ }
408+
409+--TODO: Do this in terms of a lens instead
410+lensEncoder :: (Applicative check, Monad parse)
411+ => (b -> [a] -> b) -> (b -> [a]) -> Encoder check parse (c, [a]) [a] -> Encoder check parse (c, b) b
412+lensEncoder set get g = Encoder $ do
413+ gImpl <- unEncoder g
414+ pure $ EncoderImpl
415+ { _encoderImpl_encode = \(ma, b) -> set b $ _encoderImpl_encode gImpl (ma, get b)
416+ , _encoderImpl_decode = \b -> do
417+ (ma, la) <- _encoderImpl_decode gImpl $ get b
418+ pure (ma, set b la)
419+ }
420+
421+consEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Maybe a, [a]) [a] --TODO: Really shouldn't *always* have the [a], even in the Nothing case
422+consEncoder = unsafeMkEncoder $ EncoderImpl
423+ { _encoderImpl_encode = \(h, t) -> maybeToList h <> t
424+ , _encoderImpl_decode = pure . \case
425+ [] -> (Nothing, [])
426+ h:t -> (Just h, t)
427+ }
428+
429+tshow :: Show a => a -> Text
430+tshow = T.pack . show
431+
432+shadowEncoder
433+ :: ( Universe a
434+ , MonadError Text check
435+ , Show a
436+ , Show b
437+ , Show c
438+ , check ~ parse --TODO: Get rid of this
439+ )
440+ => Encoder check parse a c -- ^ Overlaps; should have a small number of possible routes
441+ -> Encoder check parse b c -- ^ Gets overlapped
442+ -> Encoder check parse (Either a b) c
443+shadowEncoder f g = Encoder $ do
444+ vf <- unEncoder f
445+ vg <- unEncoder g
446+ let gCanParse c = catchError (Just <$> _encoderImpl_decode vg c) (\_ -> pure Nothing)
447+ overlaps <- fmap catMaybes $ forM universe $ \a -> do
448+ let c = _encoderImpl_encode vf a
449+ mb <- gCanParse c
450+ pure $ fmap (\b -> (a, b, c)) mb
451+ case overlaps of
452+ [] -> pure ()
453+ _ -> throwError $ "shadowEncoder: overlap detected: " <> T.unlines
454+ (flip fmap overlaps $ \(a, b, c) -> "first encoder encodes " <> tshow a <> " as " <> tshow c <> ", which second encoder decodes as " <> tshow b)
455+ pure $ EncoderImpl
456+ { _encoderImpl_encode = \case
457+ Left a -> _encoderImpl_encode vf a
458+ Right b -> _encoderImpl_encode vg b
459+ , _encoderImpl_decode = \c -> (Left <$> _encoderImpl_decode vf c) `catchError` \_ -> Right <$> _encoderImpl_decode vg c
460+ }
461+
462+enum1Encoder
463+ :: ( Universe (Some p)
464+ , GShow p
465+ , GCompare p
466+ , MonadError Text check
467+ , MonadError Text parse
468+ , Ord r
469+ , Show r
470+ )
471+ => (forall a. p a -> r) -> Encoder check parse (Some p) r
472+enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
473+
474+-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
475+-- have a large number of values - it will use a lot of memory.
476+enumEncoder :: forall parse check p r. (Universe p, Show p, Ord p, Ord r, MonadError Text parse, MonadError Text check, Show r) => (p -> r) -> Encoder check parse p r
477+enumEncoder f = Encoder $ do
478+ let reversed = Map.fromListWith (<>) [ (f p, Set.singleton p) | p <- universe ]
479+ checkSingleton k vs = case Set.toList vs of
480+ [] -> error "enumEncoder: empty reverse mapping; should be impossible"
481+ [e] -> Success e
482+ _ -> Failure $ Map.singleton k vs
483+ showRedundant :: r -> Set p -> [Text]
484+ showRedundant k vs = (" " <> tshow k <> " can decode to any of:")
485+ : fmap ((" "<>) . tshow) (Set.toList vs)
486+ case itraverse checkSingleton reversed :: Validation (Map r (Set p)) (Map r p) of
487+ Failure ambiguousEntries -> throwError $ T.unlines $
488+ "enumEncoder: ambiguous encodings detected:" : concat (Map.elems $ imap showRedundant ambiguousEntries)
489+ Success m -> pure $ EncoderImpl
490+ { _encoderImpl_decode = \r -> case Map.lookup r m of
491+ Just a -> pure a
492+ Nothing -> throwError $ "enumEncoder: not recognized: " <> tshow r --TODO: Report this as a better type
493+ , _encoderImpl_encode = f
494+ }
495+
496+unitEncoder :: (Applicative check, MonadError Text parse, Show r, Eq r) => r -> Encoder check parse () r
497+unitEncoder expected = unsafeMkEncoder $ EncoderImpl
498+ { _encoderImpl_decode = \obtained ->
499+ if obtained == expected
500+ then pure ()
501+ else throwError $ "endEncoderImpl: expected " <> tshow expected <> ", got " <> tshow obtained
502+ , _encoderImpl_encode = \_ -> expected
503+ }
504+
505+singlePathSegmentEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse Text PageName
506+singlePathSegmentEncoder = pathOnlyEncoder . singletonListEncoder
507+
508+pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
509+pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
510+ { _encoderImpl_decode = \(path, query) ->
511+ if query == mempty
512+ then pure path
513+ else throwError "pathOnlyEncoderImpl: query was provided"
514+ , _encoderImpl_encode = \path -> (path, mempty)
515+ }
516+
517+singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
518+singletonListEncoder = unsafeMkEncoder $ EncoderImpl
519+ { _encoderImpl_decode = \case
520+ [a] -> pure a
521+ l -> throwError $ "singletonListEncoderImpl: expected one item, got " <> tshow (length l)
522+ , _encoderImpl_encode = (:[])
523+ }
524+
525+splitTextNonEmpty :: Text -> Text -> NonEmpty Text
526+splitTextNonEmpty separator v = case T.splitOn separator v of
527+ [] -> error "splitTextNonEmpty: Data.Text.splitOn should never return an empty list"
528+ h : t -> h :| t
529+
530+--TODO: To know this is reversible, we must know that the separator isn't included anywhere in the input text
531+pathSegmentsTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse (NonEmpty Text) Text
532+pathSegmentsTextEncoder = unsafeMkEncoder $ EncoderImpl
533+ { _encoderImpl_encode = T.intercalate "/" . fmap (urlEncodeText False) . toList
534+ , _encoderImpl_decode = pure . fmap (urlDecodeText False) . splitTextNonEmpty "/"
535+ }
536+
537+queryParametersTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse [(Text, Maybe Text)] Text
538+queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
539+ { _encoderImpl_encode = \case
540+ [] -> ""
541+ params -> T.intercalate "&" (fmap encodeParameter params)
542+ , _encoderImpl_decode = pure . \case
543+ "" -> []
544+ encoded ->
545+ let h :| t = splitTextNonEmpty "&" encoded
546+ in fmap decodeParameter $ h : t
547+ }
548+ where
549+ encodeParameter (k, mv) = urlEncodeText True k <> case mv of
550+ Nothing -> ""
551+ Just v -> "=" <> urlEncodeText True v
552+ decodeParameter t =
553+ let (k, eqV) = T.breakOn "=" t
554+ mv = T.stripPrefix "=" eqV
555+ in (urlDecodeText True k, urlDecodeText True <$> mv)
556+
557+urlEncodeText :: Bool -> Text -> Text
558+urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
559+
560+urlDecodeText :: Bool -> Text -> Text
561+urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
562+
563+listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
564+listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
565+ { _encoderImpl_encode = \case
566+ [] -> mempty :| []
567+ h : t -> h :| t
568+ , _encoderImpl_decode = \(h :| t) -> pure $
569+ if h == mempty
570+ then []
571+ else h : t
572+ }
573+
574+prefixTextEncoder :: (Applicative check, MonadError Text parse) => Text -> Encoder check parse Text Text
575+prefixTextEncoder p = Encoder $ pure $ EncoderImpl
576+ { _encoderImpl_encode = mappend p
577+ , _encoderImpl_decode = \v -> case T.stripPrefix p v of
578+ Nothing -> throwError $ "prefixTextEncoder: wrong prefix; expected " <> tshow p <> ", got " <> tshow (T.take (T.length p) v)
579+ Just stripped -> pure stripped
580+ }
581+
582+prefixNonemptyTextEncoder :: (Applicative check, MonadError Text parse) => Text -> Encoder check parse Text Text
583+prefixNonemptyTextEncoder p = Encoder $ pure $ EncoderImpl
584+ { _encoderImpl_encode = \case
585+ "" -> ""
586+ v -> p <> v
587+ , _encoderImpl_decode = \case
588+ "" -> pure ""
589+ v -> case T.stripPrefix p v of
590+ Nothing -> throwError $ "prefixTextEncoder: wrong prefix; expected " <> tshow p
591+ Just stripped -> pure stripped
592+ }
593+
594+unpackTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse Text String
595+unpackTextEncoder = Encoder $ pure $ EncoderImpl
596+ { _encoderImpl_encode = T.unpack
597+ , _encoderImpl_decode = pure . T.pack
598+ }
599+
600+toListMapEncoder :: (Applicative check, Applicative parse, Ord k) => Encoder check parse (Map k v) [(k, v)]
601+toListMapEncoder = Encoder $ pure $ EncoderImpl
602+ { _encoderImpl_encode = Map.toList
603+ , _encoderImpl_decode = pure . Map.fromList --TODO: Should we be stricter about repeated keys?
604+ }
605+
606+joinPairTextEncoder :: (MonadError Text check, MonadError Text parse) => Text -> Encoder check parse (Text, Text) Text
607+joinPairTextEncoder = Encoder . \case
608+ "" -> throwError "joinPairTextEncoder: empty separator"
609+ separator -> pure $ EncoderImpl
610+ { _encoderImpl_encode = \(k, v) -> k <> separator <> v
611+ , _encoderImpl_decode = \r ->
612+ let (kt, vt) = T.breakOn separator r
613+ in case vt of
614+ -- The separator was not found
615+ "" -> throwError $ "joinPairTextEncoder: separator not found; expected " <> tshow separator
616+ _ -> return (kt, T.drop (T.length separator) vt)
617+ }
618+
619+--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
620+-- Or, more likely, the user can compose it themselves
621+rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
622+rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
623+
624+-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
625+-- if the prism doesn't match.
626+prismEncoder :: (Applicative check, MonadError Text parse) => Prism' b a -> Encoder check parse a b
627+prismEncoder p = unsafeMkEncoder $ EncoderImpl
628+ { _encoderImpl_encode = (^. re p)
629+ , _encoderImpl_decode = \r -> case r ^? p of
630+ Just a -> pure a
631+ Nothing -> throwError "prismEncoder: value is not present in the prism"
632+ }
633+
634+-- | A URL path and query string, in which trailing slashes don't matter in the path
635+-- and duplicate query parameters are not allowed. A final goal of encoders using this library
636+-- will frequently be to produce this.
637+type PageName = ([Text], Map Text (Maybe Text))
638+
639+-- | A path (separated by slashes), and a query string.
640+type PathQuery = (String, String)
641+
642+-- | Encode a PageName into a path and query string.
643+pageNameEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse PageName PathQuery
644+pageNameEncoder = bimap
645+ (unpackTextEncoder . prefixTextEncoder "/" . pathSegmentsTextEncoder . listToNonEmptyEncoder)
646+ (unpackTextEncoder . prefixNonemptyTextEncoder "?" . queryParametersTextEncoder . toListMapEncoder)
647+
648+-- | Handle an error in parsing, for example, in order to redirect to a 404 page.
649+handleEncoder
650+ :: (Functor check)
651+ => (e -> a)
652+ -> Encoder check (Either e) a b
653+ -> Encoder check Identity a b
654+handleEncoder recover e = Encoder $ do
655+ i <- unEncoder e
656+ return $ i
657+ { _encoderImpl_decode = \a -> pure $ case _encoderImpl_decode i a of
658+ Right r -> r
659+ Left err -> recover err
660+ }
661+
662+--------------------------------------------------------------------------------
663+-- Actual obelisk route info
664+--------------------------------------------------------------------------------
665+
666+-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
667+-- frontend.
668+data ObeliskRoute :: (* -> *) -> * -> * where
669+ -- We need to have the `f a` as an argument here, because otherwise we have no way to specifically check for overlap between us and the given encoder
670+ ObeliskRoute_App :: f a -> ObeliskRoute f a
671+ ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
672+
673+instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
674+ universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
675+ ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
676+
677+instance GEq f => GEq (ObeliskRoute f) where
678+ geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
679+ geq (ObeliskRoute_Resource x) (ObeliskRoute_Resource y) = geq x y
680+ geq _ _ = Nothing
681+
682+instance GCompare f => GCompare (ObeliskRoute f) where
683+ gcompare (ObeliskRoute_App x) (ObeliskRoute_App y) = gcompare x y
684+ gcompare (ObeliskRoute_Resource x) (ObeliskRoute_Resource y) = gcompare x y
685+ gcompare (ObeliskRoute_App _) (ObeliskRoute_Resource _) = GLT
686+ gcompare (ObeliskRoute_Resource _) (ObeliskRoute_App _) = GGT
687+
688+-- | A type representing the various resource routes served by Obelisk. These can in principle map to any physical routes you want,
689+-- but sane defaults are provided by 'resourceRouteSegment'
690+data ResourceRoute :: * -> * where
691+ ResourceRoute_Static :: ResourceRoute [Text] -- This [Text] represents the *path in our static files directory*, not necessarily the URL path that the asset gets served at (although that will often be "/static/this/text/thing")
692+ ResourceRoute_Ghcjs :: ResourceRoute [Text]
693+ ResourceRoute_JSaddleWarp :: ResourceRoute (R JSaddleWarpRoute)
694+
695+-- | If there are no additional backend routes in your app (i.e. ObeliskRoute gives you all the routes you need),
696+-- this constructs a suitable 'Encoder' to use for encoding routes to 'PageName's. If you do have additional backend routes,
697+-- you'll want to use 'pathComponentEncoder' yourself, applied to a function that will likely use obeliskRouteSegment in order to
698+-- handle the ObeliskRoute case (i.e. Obelisk resource routes and app frontend routes).
699+obeliskRouteEncoder :: forall check parse appRoute.
700+ ( Universe (Some (ObeliskRoute appRoute))
701+ , GCompare (ObeliskRoute appRoute)
702+ , GShow appRoute
703+ , MonadError Text check
704+ , check ~ parse --TODO: Get rid of this
705+ )
706+ => (forall a. appRoute a -> SegmentResult check parse a)
707+ -> Encoder check parse (R (ObeliskRoute appRoute)) PageName
708+obeliskRouteEncoder appRouteSegment = pathComponentEncoder $ \r ->
709+ obeliskRouteSegment r appRouteSegment
710+
711+-- | From a function which explains how app-specific frontend routes translate into segments, produce a function which does the
712+-- same for ObeliskRoute. This uses the given function for the 'ObeliskRoute_App' case, and 'resourceRouteSegment' for the
713+-- 'ObeliskRoute_Resource' case.
714+obeliskRouteSegment :: forall check parse appRoute a.
715+ (MonadError Text check, MonadError Text parse)
716+ => ObeliskRoute appRoute a
717+ -> (forall a. appRoute a -> SegmentResult check parse a)
718+ -> SegmentResult check parse a
719+obeliskRouteSegment r appRouteSegment = case r of
720+ ObeliskRoute_App appRoute -> appRouteSegment appRoute
721+ ObeliskRoute_Resource resourceRoute -> resourceRouteSegment resourceRoute
722+
723+-- | A function which gives a sane default for how to encode Obelisk resource routes. It's given in this form, because it will
724+-- be combined with other such segment encoders before 'pathComponentEncoder' turns it into a proper 'Encoder'.
725+resourceRouteSegment :: (MonadError Text check, MonadError Text parse) => ResourceRoute a -> SegmentResult check parse a
726+resourceRouteSegment = \case
727+ ResourceRoute_Static -> PathSegment "static" $ pathOnlyEncoder
728+ ResourceRoute_Ghcjs -> PathSegment "ghcjs" $ pathOnlyEncoder
729+ ResourceRoute_JSaddleWarp -> PathSegment "jsaddle" $ jsaddleWarpRouteEncoder
730+
731+data JSaddleWarpRoute :: * -> * where
732+ JSaddleWarpRoute_JavaScript :: JSaddleWarpRoute ()
733+ JSaddleWarpRoute_WebSocket :: JSaddleWarpRoute ()
734+ JSaddleWarpRoute_Sync :: JSaddleWarpRoute [Text]
735+
736+jsaddleWarpRouteEncoder :: (MonadError Text check, MonadError Text parse) => Encoder check parse (R JSaddleWarpRoute) PageName
737+jsaddleWarpRouteEncoder = pathComponentEncoder $ \case
738+ JSaddleWarpRoute_JavaScript -> PathSegment "jsaddle.js" $ unitEncoder mempty
739+ JSaddleWarpRoute_WebSocket -> PathEnd $ unitEncoder mempty
740+ JSaddleWarpRoute_Sync -> PathSegment "sync" $ pathOnlyEncoder
741+
742+instance GShow appRoute => GShow (ObeliskRoute appRoute) where
743+ gshowsPrec prec = \case
744+ ObeliskRoute_App appRoute -> showParen (prec > 10) $
745+ showString "ObeliskRoute_App " . gshowsPrec 11 appRoute
746+ ObeliskRoute_Resource appRoute -> showParen (prec > 10) $
747+ showString "ObeliskRoute_Resource " . gshowsPrec 11 appRoute
748+
749+data IndexOnlyRoute :: * -> * where
750+ IndexOnlyRoute :: IndexOnlyRoute ()
751+
752+indexOnlyRouteSegment :: (Applicative check, MonadError Text parse) => IndexOnlyRoute a -> SegmentResult check parse a
753+indexOnlyRouteSegment = \case
754+ IndexOnlyRoute -> PathEnd $ unitEncoder mempty
755+
756+indexOnlyRouteEncoder :: (MonadError Text check, MonadError Text parse) => Encoder check parse (R IndexOnlyRoute) PageName
757+indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
758+
759+someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
760+someSumEncoder = Encoder $ pure $ EncoderImpl
761+ { _encoderImpl_encode = \(Some.This t) -> case t of
762+ InL l -> Left $ Some.This l
763+ InR r -> Right $ Some.This r
764+ , _encoderImpl_decode = pure . \case
765+ Left (Some.This l) -> Some.This (InL l)
766+ Right (Some.This r) -> Some.This (InR r)
767+ }
768+
769+data Void1 :: * -> * where {}
770+
771+instance Universe (Some Void1) where
772+ universe = []
773+
774+void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
775+void1Encoder = Encoder $ pure $ EncoderImpl
776+ { _encoderImpl_encode = \case
777+ Some.This f -> case f of {}
778+ , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
779+ }
780+
781+instance GShow Void1 where
782+ gshowsPrec _ = \case {}
783+
784+concat <$> mapM deriveRouteComponent
785+ [ ''ResourceRoute
786+ , ''JSaddleWarpRoute
787+ , ''IndexOnlyRoute
788+ ]
789+
790+makePrisms ''ObeliskRoute
791+
792+deriveGEq ''Void1
793+deriveGCompare ''Void1
794+
795+--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hsnew file+322
@@ -0,0 +1,322 @@
1+{-# LANGUAGE ApplicativeDo #-}
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE RecursiveDo #-}
4+{-# LANGUAGE StandaloneDeriving #-}
5+{-# LANGUAGE ConstraintKinds #-}
6+{-# LANGUAGE FlexibleContexts #-}
7+{-# LANGUAGE FlexibleInstances #-}
8+{-# LANGUAGE GADTs #-}
9+{-# LANGUAGE KindSignatures #-}
10+{-# LANGUAGE LambdaCase #-}
11+{-# LANGUAGE MultiParamTypeClasses #-}
12+{-# LANGUAGE OverloadedStrings #-}
13+{-# LANGUAGE PatternSynonyms #-}
14+{-# LANGUAGE RankNTypes #-}
15+{-# LANGUAGE ScopedTypeVariables #-}
16+{-# LANGUAGE TemplateHaskell #-}
17+{-# LANGUAGE TypeFamilies #-}
18+{-# LANGUAGE TypeOperators #-}
19+{-# LANGUAGE ViewPatterns #-}
20+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
21+{-# LANGUAGE UndecidableInstances #-}
22+{-# LANGUAGE DataKinds #-}
23+{-# LANGUAGE FunctionalDependencies #-}
24+
25+module Obelisk.Route.Frontend
26+ ( module Obelisk.Route
27+ , pattern (:~)
28+ , Routed
29+ , RoutedT
30+ , runRoutedT
31+ , askRoute
32+ , withRoutedT
33+ , mapRoutedT
34+ , subRoute
35+ , subRoute_
36+ , runRouteViewT
37+ , SetRouteT(..)
38+ , SetRoute(..)
39+ , runSetRouteT
40+ , mapSetRouteT
41+ ) where
42+
43+import Prelude hiding ((.), id)
44+
45+import Obelisk.Route
46+
47+import Control.Category (Category (..), (.))
48+import Control.Category.Cartesian
49+import Control.Lens hiding (Bifunctor, bimap, universe)
50+import Control.Monad.Fix
51+import Control.Monad.Primitive
52+import Control.Monad.Ref
53+import Control.Monad.Trans
54+import Control.Monad.Trans.Control
55+import Control.Monad.Trans.Reader
56+import Data.Coerce
57+import Data.Constraint (Dict (..))
58+import Data.Dependent.Sum (DSum (..))
59+import Data.GADT.Compare
60+import Data.Monoid
61+import qualified Data.Text as T
62+import Data.Functor.Compose
63+import Reflex.Class
64+import Reflex.Host.Class
65+import Reflex.PostBuild.Class
66+import Reflex.TriggerEvent.Class
67+import Reflex.PerformEvent.Class
68+import Reflex.EventWriter.Class
69+import Reflex.EventWriter.Base
70+import Reflex.Dynamic
71+import Reflex.Dom.Builder.Class
72+import Data.Type.Coercion
73+import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
74+import Reflex.Dom.Core
75+import qualified GHCJS.DOM.Types as DOM
76+import Network.URI
77+#if defined(ios_HOST_OS)
78+import Data.Maybe (fromMaybe)
79+import qualified Data.List as L
80+#endif
81+
82+import Unsafe.Coerce
83+
84+infixr 5 :~
85+pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
86+pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
87+
88+class Routed t r m where
89+ askRoute :: m (Dynamic t r)
90+
91+instance Monad m => Routed t r (RoutedT t r m) where
92+ askRoute = RoutedT ask
93+
94+newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
95+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, HasJSContext, MonadIO, MonadReflexCreateTrigger t, HasDocument)
96+
97+instance Prerender js m => Prerender js (RoutedT t r m) where
98+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
99+
100+instance Requester t m => Requester t (RoutedT t r m) where
101+ type Request (RoutedT t r m) = Request m
102+ type Response (RoutedT t r m) = Response m
103+ requesting = RoutedT . requesting
104+ requesting_ = RoutedT . requesting_
105+
106+#ifndef ghcjs_HOST_OS
107+deriving instance MonadJSM m => MonadJSM (RoutedT t r m)
108+#endif
109+
110+instance PerformEvent t m => PerformEvent t (RoutedT t r m) where
111+ type Performable (RoutedT t r m) = Performable m
112+ performEvent = lift . performEvent
113+ performEvent_ = lift . performEvent_
114+
115+instance MonadRef m => MonadRef (RoutedT t r m) where
116+ type Ref (RoutedT t r m) = Ref m
117+ newRef = lift . newRef
118+ readRef = lift . readRef
119+ writeRef r = lift . writeRef r
120+
121+instance HasJS x m => HasJS x (RoutedT t r m) where
122+ type JSX (RoutedT t r m) = JSX m
123+ liftJS = lift . liftJS
124+
125+deriving instance EventWriter t w m => EventWriter t w (RoutedT t r m)
126+
127+instance MonadTransControl (RoutedT t r) where
128+ type StT (RoutedT t r) a = StT (ReaderT (Dynamic t r)) a
129+ liftWith = defaultLiftWith RoutedT unRoutedT
130+ restoreT = defaultRestoreT RoutedT
131+
132+instance PrimMonad m => PrimMonad (RoutedT t r m ) where
133+ type PrimState (RoutedT t r m) = PrimState m
134+ primitive = lift . primitive
135+
136+instance DomBuilder t m => DomBuilder t (RoutedT t r m) where
137+ type DomBuilderSpace (RoutedT t r m) = DomBuilderSpace m
138+
139+instance Adjustable t m => Adjustable t (RoutedT t r m) where
140+ runWithReplace a0 a' = RoutedT $ runWithReplace (coerce a0) $ coerceEvent a'
141+ traverseIntMapWithKeyWithAdjust f a0 a' = RoutedT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
142+ traverseDMapWithKeyWithAdjust f a0 a' = RoutedT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
143+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = RoutedT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
144+
145+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
146+ tellQueryIncremental = lift . tellQueryIncremental
147+ askQueryResult = lift askQueryResult
148+ queryIncremental = lift . queryIncremental
149+
150+runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
151+runRoutedT = runReaderT . unRoutedT
152+
153+mapRoutedT :: (m a -> n a) -> RoutedT t r m a -> RoutedT t r n a
154+mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
155+
156+withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
157+withRoutedT f = RoutedT . withReaderT f . unRoutedT
158+
159+subRoute_ :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r a -> RoutedT t a m ()) -> RoutedT t (R r) m ()
160+subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
161+ runRoutedT (f c) r'
162+
163+subRoute :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r a -> RoutedT t a m b) -> RoutedT t (R r) m (Dynamic t b)
164+subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
165+ runRoutedT (f c) r'
166+
167+dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
168+dsumValueCoercion Coercion = Coercion
169+
170+dynamicIdentityCoercion :: Coercion (Compose (Dynamic t) Identity) (Dynamic t)
171+dynamicIdentityCoercion = unsafeCoerce (Coercion :: Coercion (Identity ()) ()) --TODO: Is it possible to prove this?
172+
173+factorRouted :: (Reflex t, MonadFix m, MonadHold t m, GEq f) => RoutedT t (DSum f (Dynamic t)) m a -> RoutedT t (DSum f Identity) m a
174+factorRouted r = RoutedT $ ReaderT $ \d -> do
175+ d' <- factorDyn d
176+ runRoutedT r $ (coerceWith (dynamicCoercion $ dsumValueCoercion dynamicIdentityCoercion) d')
177+
178+-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
179+strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
180+strictDynWidget f = RoutedT $ ReaderT $ \r -> do
181+ r0 <- sample $ current r
182+ (result0, result') <- runWithReplace (f r0) $ f <$> updated r
183+ holdDyn result0 result'
184+
185+strictDynWidget_ :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m ()) -> RoutedT t a m ()
186+strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
187+ r0 <- sample $ current r
188+ (_, _) <- runWithReplace (f r0) $ f <$> updated r
189+ pure ()
190+
191+newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
192+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, HasJSContext, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomBuilder t)
193+
194+mapSetRouteT :: (forall x. m x -> n x) -> SetRouteT t r m a -> SetRouteT t r n a
195+mapSetRouteT f (SetRouteT x) = SetRouteT (mapEventWriterT f x)
196+
197+runSetRouteT :: (Reflex t, Monad m) => SetRouteT t r m a -> m (a, Event t (Endo r))
198+runSetRouteT = runEventWriterT . unSetRouteT
199+
200+class Reflex t => SetRoute t r m | m -> t r where
201+ setRoute :: Event t r -> m ()
202+ modifyRoute :: Event t (r -> r) -> m ()
203+ setRoute = modifyRoute . fmap const
204+
205+instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
206+ modifyRoute = SetRouteT . tellEvent . fmap Endo
207+
208+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
209+ modifyRoute = lift . modifyRoute
210+
211+instance Prerender js m => Prerender js (SetRouteT t r m) where
212+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
213+
214+instance Requester t m => Requester t (SetRouteT t r m) where
215+ type Request (SetRouteT t r m) = Request m
216+ type Response (SetRouteT t r m) = Response m
217+ requesting = SetRouteT . requesting
218+ requesting_ = SetRouteT . requesting_
219+
220+#ifndef ghcjs_HOST_OS
221+deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
222+#endif
223+
224+instance PerformEvent t m => PerformEvent t (SetRouteT t r m) where
225+ type Performable (SetRouteT t r m) = Performable m
226+ performEvent = lift . performEvent
227+ performEvent_ = lift . performEvent_
228+
229+instance MonadRef m => MonadRef (SetRouteT t r m) where
230+ type Ref (SetRouteT t r m) = Ref m
231+ newRef = lift . newRef
232+ readRef = lift . readRef
233+ writeRef r = lift . writeRef r
234+
235+instance HasJS x m => HasJS x (SetRouteT t r m) where
236+ type JSX (SetRouteT t r m) = JSX m
237+ liftJS = lift . liftJS
238+
239+instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
240+ type PrimState (SetRouteT t r m) = PrimState m
241+ primitive = lift . primitive
242+
243+instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
244+ runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
245+ traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
246+ traverseDMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
247+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = SetRouteT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
248+
249+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
250+ tellQueryIncremental = lift . tellQueryIncremental
251+ askQueryResult = lift askQueryResult
252+ queryIncremental = lift . queryIncremental
253+
254+runRouteViewT
255+ :: forall t m r a.
256+ ( TriggerEvent t m
257+ , PerformEvent t m
258+ , MonadHold t m
259+ , MonadJSM m
260+ , MonadJSM (Performable m)
261+ , MonadFix m
262+ )
263+ => (Encoder Identity Identity r PageName)
264+ -> RoutedT t r (SetRouteT t r m) a
265+ -> m a
266+runRouteViewT routeEncoder a = do
267+ rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
268+ let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
269+ -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
270+ -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
271+ -- this ever to happen, we'll just handle it by failing completely with 'error'.
272+ route :: Dynamic t r
273+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
274+ where
275+ errorLeft (Left e) = error (T.unpack e)
276+ errorLeft (Right x) = x
277+ (result, changeState) <- runSetRouteT $ runRoutedT a route
278+ let f (currentHistoryState, oldRoute) change =
279+ let newRoute = appEndo change oldRoute
280+ (newPath, newQuery) = encode theEncoder newRoute
281+ in HistoryStateUpdate
282+ { _historyStateUpdate_state = DOM.SerializedScriptValue jsNull
283+ -- We always provide "" as the title. On Firefox, Chrome, and
284+ -- Edge, this parameter does nothing. On Safari, "" has the
285+ -- same behavior as other browsers (as far as I can tell), but
286+ -- anything else sets the title for the back button list item
287+ -- the *next* time pushState is called, unless the page title
288+ -- is changed in the interim. Since the Safari functionality
289+ -- is near-pointless and also confusing, I'm not going to even
290+ -- bother exposing it; if there ends up being a real use case,
291+ -- we can change this function later to accommodate.
292+ -- See: https://github.com/whatwg/html/issues/2174
293+ , _historyStateUpdate_title = ""
294+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
295+ { uriQuery = newQuery
296+ }
297+ }
298+ setState = attachWith f ((,) <$> current historyState <*> current route) changeState
299+ return result
300+
301+-- On ios due to sandboxing when loading the page from a file adapt the
302+-- path to be based on the hash.
303+
304+adaptedUriPath :: URI -> String
305+#if defined(ios_HOST_OS)
306+adaptedUriPath = hashToPath . uriFragment
307+
308+hashToPath :: String -> String
309+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
310+#else
311+adaptedUriPath = uriPath
312+#endif
313+
314+setAdaptedUriPath :: String -> URI -> URI
315+#if defined(ios_HOST_OS)
316+setAdaptedUriPath s u = u { uriFragment = pathToHash s }
317+
318+pathToHash :: String -> String
319+pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
320+#else
321+setAdaptedUriPath s u = u { uriPath = s }
322+#endif
lib/route/src/Obelisk/Route/TH.hsnew file+18
@@ -0,0 +1,18 @@
1+module Obelisk.Route.TH (deriveRouteComponent) where
2+
3+import Data.GADT.Show.TH
4+import Data.GADT.Compare.TH
5+import Data.Universe.TH
6+import Language.Haskell.TH
7+
8+-- | Derive all the typeclasses needed for a RouteComponent type. The argument should be the name of a type of kind @k -> *@
9+deriveRouteComponent :: Name -> Q [Dec]
10+deriveRouteComponent x = concat <$> traverse ($ x)
11+ [ deriveGShow
12+ , deriveGEq
13+ , deriveGCompare
14+ , deriveShowTagIdentity
15+ , deriveEqTagIdentity
16+ , deriveOrdTagIdentity
17+ , deriveSomeUniverse
18+ ]
lib/run/obelisk-run.cabal+13−1
@@ -10,6 +10,9 @@ library
1010 attoparsec
1111 , base
1212 , bytestring
13+ , dependent-sum
14+ , dependent-sum-template
15+ , ghcjs-dom
1316 , http-client
1417 , http-reverse-proxy
1518 , http-types
@@ -17,16 +20,25 @@ library
1720 , jsaddle-warp
1821 , lens
1922 , modern-uri
23+ , mtl
2024 , network
25+ , obelisk-backend
2126 , obelisk-executable-config
27+ , obelisk-executable-config-inject
28+ , obelisk-frontend
29+ , obelisk-route
2230 , process
31+ , ref-tf
32+ , reflex
2333 , reflex-dom
34+ , snap-core
2435 , streaming-commons
2536 , text
37+ , universe
2638 , wai
2739 , wai-websockets
2840 , warp
2941 , websockets
3042 exposed-modules:
3143 Obelisk.Run
32− ghc-options: -Wall
44+ ghc-options: -Wall -fwarn-redundant-constraints
lib/run/src/Obelisk/Run.hs+79−36
@@ -1,19 +1,40 @@
1+{-# LANGUAGE ConstraintKinds #-}
2+{-# LANGUAGE EmptyCase #-}
3+{-# LANGUAGE EmptyDataDecls #-}
4+{-# LANGUAGE FlexibleContexts #-}
5+{-# LANGUAGE FlexibleInstances #-}
6+{-# LANGUAGE GADTs #-}
7+{-# LANGUAGE KindSignatures #-}
18{-# LANGUAGE LambdaCase #-}
9+{-# LANGUAGE MultiParamTypeClasses #-}
210{-# LANGUAGE OverloadedStrings #-}
11+{-# LANGUAGE Rank2Types #-}
12+{-# LANGUAGE RecursiveDo #-}
313{-# LANGUAGE ScopedTypeVariables #-}
14+{-# LANGUAGE TemplateHaskell #-}
15+{-# LANGUAGE TypeFamilies #-}
16+{-# LANGUAGE UndecidableInstances #-}
17+{-# OPTIONS_GHC -fno-warn-orphans #-} -- Due to instance HasJS x (EventWriterT t w m)
418module Obelisk.Run where
519
20+import Prelude hiding ((.), id)
21+
22+import Control.Category
623import Control.Concurrent
724import Control.Exception
8−import Control.Lens ((^?), _Just, _Right)
25+import Control.Lens ((%~), (^?), _Just, _Right)
926import qualified Data.Attoparsec.ByteString.Char8 as A
1027import Data.ByteString (ByteString)
1128import qualified Data.ByteString.Char8 as BSC
1229import qualified Data.ByteString.Lazy.Char8 as BSLC
30+import Data.Dependent.Sum (DSum (..))
31+import Data.Functor.Identity
32+import Data.Functor.Sum
1333import Data.List (uncons)
1434import Data.Maybe
1535import Data.Semigroup ((<>))
1636import Data.Streaming.Network (bindPortTCP)
37+import Data.Text (Text)
1738import qualified Data.Text as T
1839import Language.Javascript.JSaddle.Run (syncPoint)
1940import Language.Javascript.JSaddle.WebSockets
@@ -25,10 +46,12 @@ import Network.Wai (Application)
2546import qualified Network.Wai as W
2647import Network.Wai.Handler.Warp
2748import Network.Wai.Handler.Warp.Internal (settingsHost, settingsPort)
28−import Network.Wai.Handler.WebSockets (isWebSocketsReq)
2949import Network.WebSockets (ConnectionOptions)
3050import Network.WebSockets.Connection (defaultConnectionOptions)
3151import Obelisk.ExecutableConfig (get)
52+import Obelisk.ExecutableConfig.Inject (injectExecutableConfigs)
53+import Obelisk.Frontend
54+import Obelisk.Route.Frontend
3255import Reflex.Dom.Core
3356import System.Environment
3457import System.IO
@@ -36,20 +59,32 @@ import System.Process
3659import Text.URI (URI)
3760import qualified Text.URI as URI
3861import Text.URI.Lens
62+import Obelisk.Backend
3963
40−run :: Int -- ^ Port to run the backend
41− -> IO () -- ^ Backend
42− -> (StaticWidget () (), Widget () ()) -- ^ Frontend widget (head, body)
43− -> IO ()
64+run
65+ :: Int -- ^ Port to run the backend
66+ -> Backend fullRoute frontendRoute -- ^ Backend
67+ -> Frontend (R frontendRoute) -- ^ Frontend
68+ -> IO ()
4469run port backend frontend = do
70+ prettifyOutput
4571 let handleBackendErr (e :: IOException) = hPutStrLn stderr $ "backend stopped; make a change to your code to reload - error " <> show e
46− backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] backend
47− putStrLn $ "Backend running on port " <> show port
48− let conf = defRunConfig { _runConfig_redirectPort = port }
49− runWidget conf frontend `finally` killThread backendTid
72+ --TODO: Use Obelisk.Backend.runBackend; this will require separating the checking and running phases
73+ case checkEncoder $ _backend_routeEncoder backend of
74+ Left e -> hPutStrLn stderr $ "backend error:\n" <> T.unpack e
75+ Right validFullEncoder -> do
76+ backendTid <- forkIO $ handle handleBackendErr $ withArgs ["--quiet", "--port", show port] $ do
77+ _backend_run backend $ \serveRoute -> do
78+ runSnapWithCommandLineArgs $ do
79+ getRouteWith validFullEncoder >>= \case
80+ Identity r -> case r of
81+ InL backendRoute :=> Identity a -> serveRoute $ backendRoute :/ a
82+ InR obeliskRoute :=> Identity a -> serveDefaultObeliskApp frontend $ obeliskRoute :/ a
83+ let conf = defRunConfig { _runConfig_redirectPort = port }
84+ runWidget conf frontend validFullEncoder `finally` killThread backendTid
5085
5186getConfigRoute :: IO (Maybe URI)
52−getConfigRoute = get "common/route" >>= \case
87+getConfigRoute = get "config/common/route" >>= \case
5388 Just r -> case URI.mkURI $ T.strip r of
5489 Just route -> pure $ Just route
5590 Nothing -> do
@@ -60,8 +95,8 @@ getConfigRoute = get "common/route" >>= \case
6095defAppUri :: URI
6196defAppUri = fromMaybe (error "defAppUri") $ URI.mkURI "http://127.0.0.1:8000"
6297
63−runWidget :: RunConfig -> (StaticWidget () (), Widget () ()) -> IO ()
64−runWidget conf (h, b) = do
98+runWidget :: RunConfig -> Frontend (R route) -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> IO ()
99+runWidget conf frontend validFullEncoder = do
65100 uri <- fromMaybe defAppUri <$> getConfigRoute
66101 let port = fromIntegral $ fromMaybe 80 $ uri ^? uriAuthority . _Right . authPort . _Just
67102 redirectHost = _runConfig_redirectHost conf
@@ -74,32 +109,40 @@ runWidget conf (h, b) = do
74109 close
75110 (\skt -> do
76111 man <- newManager defaultManagerSettings
77− app <- obeliskApp defaultConnectionOptions h b (fallbackProxy redirectHost redirectPort man)
112+ app <- obeliskApp defaultConnectionOptions frontend validFullEncoder uri $ fallbackProxy redirectHost redirectPort man
78113 runSettingsSocket settings skt app)
79114
80−obeliskApp :: ConnectionOptions -> StaticWidget () () -> Widget () () -> Application -> IO Application
81−obeliskApp opts h b backend = do
82− html <- BSLC.fromStrict <$> indexHtml h
83− let entryPoint = mainWidget' b >> syncPoint
84− jsaddle <- jsaddleOr opts entryPoint $ \req sendResponse -> case (W.requestMethod req, W.pathInfo req) of
85− ("GET", []) -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] html
86− ("GET", ["jsaddle.js"]) -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs False
87− _ -> backend req sendResponse
88− -- Workaround jsaddleOr wanting to handle all websockets requests without
89− -- having a chance for run to proxy non jsaddle websocket requests to the
90− -- backend.
91− return $ \req sendResponse -> do
92− if isWebSocketsReq req && not (null $ W.pathInfo req)
93− then backend req sendResponse
94− else jsaddle req sendResponse
115+obeliskApp
116+ :: forall route backendRoute. ConnectionOptions
117+ -> Frontend (R route)
118+ -> Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName
119+ -> URI
120+ -> Application
121+ -> IO Application
122+obeliskApp opts frontend validFullEncoder uri backend = do
123+ let entryPoint = do
124+ runFrontend validFullEncoder frontend
125+ syncPoint
126+ jsaddlePath <- URI.mkPathPiece "jsaddle"
127+ let jsaddleUri = BSLC.fromStrict $ URI.renderBs $ uri & uriPath %~ (<>[jsaddlePath])
128+ Right (jsaddleWarpRouteValidEncoder :: Encoder Identity (Either Text) (R JSaddleWarpRoute) PageName) <- return $ checkEncoder jsaddleWarpRouteEncoder
129+ jsaddle <- jsaddleWithAppOr opts entryPoint $ \_ sendResponse -> sendResponse $ W.responseLBS H.status500 [("Content-Type", "text/plain")] "obeliskApp: jsaddle got a bad URL"
130+ return $ \req sendResponse -> case tryDecode validFullEncoder (W.pathInfo req, mempty) of --TODO: Query strings
131+ Identity r -> case r of
132+ InR (ObeliskRoute_Resource ResourceRoute_JSaddleWarp) :=> Identity jsaddleRoute -> case jsaddleRoute of
133+ JSaddleWarpRoute_JavaScript :/ () -> sendResponse $ W.responseLBS H.status200 [("Content-Type", "application/javascript")] $ jsaddleJs' (Just jsaddleUri) False
134+ _ -> flip jsaddle sendResponse $ req
135+ { W.pathInfo = fst $ encode jsaddleWarpRouteValidEncoder jsaddleRoute
136+ }
137+ InR (ObeliskRoute_App appRouteComponent) :=> Identity appRouteRest -> do
138+ html <- renderJsaddleFrontend (appRouteComponent :/ appRouteRest) frontend
139+ sendResponse $ W.responseLBS H.status200 [("Content-Type", "text/html")] $ BSLC.fromStrict html
140+ _ -> backend req sendResponse
95141
96−indexHtml :: StaticWidget () () -> IO ByteString
97−indexHtml h = do
98− ((), bs) <- renderStatic $ el "html" $ do
99− el "head" $ h
100− el "body" $ return ()
101− elAttr "script" ("src" =: "/jsaddle.js") $ return ()
102− return $ "<!DOCTYPE html>" <> bs
142+renderJsaddleFrontend :: route -> Frontend route -> IO ByteString
143+renderJsaddleFrontend r f =
144+ let jsaddleScript = elAttr "script" ("src" =: "/jsaddle/jsaddle.js") blank
145+ in renderFrontendHtml r (_frontend_head f >> injectExecutableConfigs) (_frontend_body f >> jsaddleScript)
103146
104147-- | like 'bindPortTCP' but reconnects on exception
105148bindPortTCPRetry :: Settings
migration/07900e6c41ab9a78917f66fcf7e0d9b8-8c84f81d9b09a5e0f028f3c6341ffb36/obelisk-handoffnew file

No changes to the content.

migration/07900e6c41ab9a78917f66fcf7e0d9b8-8c84f81d9b09a5e0f028f3c6341ffb36/obelisk-upgradenew file+15
@@ -0,0 +1,15 @@
1+# ob deploy: HTTPs support
2+
3+Obelisk deployments now support HTTPs. You must upgrade the deployment repo
4+associated with the obelisk project as follows:
5+
6+```
7+cd /your/project's/deployment/repo
8+echo True > enable_https
9+echo admin@yoursite.com > admin_email
10+echo https://yoursite.com/ > config/common/route
11+git add ... && git commit ...
12+ob deploy push
13+```
14+
15+If you do not wish to enable HTTPs, write `False` to `enable_https` instead.
release.nix+28−27
@@ -1,12 +1,12 @@
1−{ self ? import ./. {}
2−, pkgs ? self.nixpkgs
1+{ local-self ? import ./. {}
2+, local-pkgs ? local-self.nixpkgs
33}:
44
5−with pkgs;
5+with local-pkgs.lib;
66
7−{
8− inherit (self) command;
9− built-skeleton = (import ./skeleton {}).all;
7+genAttrs [ "x86_64-linux" "x86_64-darwin" ] (system: {
8+ inherit (import ./. { inherit system; }) command;
9+ built-skeleton = (import ./skeleton { inherit system; }).all;
1010 tests = {
1111 #TODO: Doesn't work; see discussion in https://www.pivotaltracker.com/story/show/157265140
1212 # ob-init = runCommand "ob-init" {
@@ -20,25 +20,26 @@ with pkgs;
2020 # # ob init --symlink=${self.path}
2121 #'';
2222
23− verify-migration = runCommand "verify-migration" {
24− nativeBuildInputs = [
25− self.command
26− nix # obelisk uses `nix-hash`
27− ];
28− } ''
29− set -e
30− # Fix permissions so `removePathForcibly` works on copied-to-tmp paths
31− cp -a ${self.pathGit} $TMPDIR/obelisk
32− cd $TMPDIR/obelisk
33− chmod -R u+w .
34−
35− (set -x;
36− ob internal hash;
37− ob internal verify-migration;
38− )
39−
40− mkdir -p $out
41− touch $out/done
42− '';
23+ #TODO: Re-enable this
24+ #verify-migration = runCommand "verify-migration" {
25+ # nativeBuildInputs = [
26+ # self.command
27+ # nix # obelisk uses `nix-hash`
28+ # ];
29+ #} ''
30+ # set -e
31+ # # Fix permissions so `removePathForcibly` works on copied-to-tmp paths
32+ # cp -a ${self.pathGit} $TMPDIR/obelisk
33+ # cd $TMPDIR/obelisk
34+ # chmod -R u+w .
35+ #
36+ # (set -x;
37+ # ob internal hash;
38+ # ob internal verify-migration;
39+ # )
40+ #
41+ # mkdir -p $out
42+ # touch $out/done
43+ #'';
4344 };
44−}
45+})
scripts/ghcid-this-libnew file+4
@@ -0,0 +1,4 @@
1+#!/usr/bin/env bash
2+set -euo pipefail
3+
4+nix-shell -E "with (import ../.. {}); reflex-platform.workOn haskellPackageSets.ghc haskellPackageSets.ghc.obelisk-$(basename $PWD)" --run "cabal configure ; ghcid -c'cabal repl'"
skeleton/.obelisk/.gitignorenew file+3
@@ -0,0 +1,3 @@
1+/impl
2+!/impl/default.nix
3+!/impl/github.json
skeleton/backend/backend.cabal+2−6
@@ -11,22 +11,18 @@ library
1111 , common
1212 , frontend
1313 , obelisk-backend
14+ , obelisk-route
1415 exposed-modules:
1516 Backend
1617 ghc-options: -Wall
1718
1819executable backend
1920 main-is: main.hs
20− hs-source-dirs: src
21+ hs-source-dirs: src-bin
2122 if impl(ghcjs)
2223 buildable: False
2324 build-depends: base
2425 , backend
2526 , common
2627 , frontend
27− , lens
28− , modern-uri
2928 , obelisk-backend
30− , obelisk-executable-config
31− , obelisk-executable-config-inject
32− , text
skeleton/backend/src-bin/main.hsnew file+6
@@ -0,0 +1,6 @@
1+import Backend
2+import Frontend
3+import Obelisk.Backend
4+
5+main :: IO ()
6+main = runBackend backend frontend
skeleton/backend/src/Backend.hs+11−6
@@ -1,10 +1,15 @@
1+{-# LANGUAGE EmptyCase #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE LambdaCase #-}
4+{-# LANGUAGE RankNTypes #-}
5+{-# LANGUAGE TypeFamilies #-}
16module Backend where
27
3−import Common.Api
4−import Frontend
5−import qualified Obelisk.Backend as Ob
8+import Common.Route
9+import Obelisk.Backend
610
7−backend :: IO ()
8−backend = Ob.backend Ob.def
9− { Ob._backendConfig_head = fst frontend
11+backend :: Backend BackendRoute FrontendRoute
12+backend = Backend
13+ { _backend_run = \serve -> serve $ const $ return ()
14+ , _backend_routeEncoder = backendRouteEncoder
1015 }
skeleton/backend/src/main.hsdeleted−20
@@ -1,20 +0,0 @@
1−{-# LANGUAGE OverloadedStrings #-}
2−import Control.Exception
3−import Control.Lens
4−import Data.Maybe
5−import Data.Text.Encoding
6−import Obelisk.ExecutableConfig (get)
7−import System.Environment
8−import qualified Text.URI as URI
9−import Text.URI.Lens
10−
11−import Backend
12−
13−main :: IO ()
14−main = do
15− Just route <- get "common/route"
16− port <- case URI.mkURI route of
17− Left err -> fail $ show err
18− Right uri -> return $ fromMaybe 8000 $ uri ^? uriAuthority . _Right . authPort . _Just
19− withArgs ["--port", show port] backend
20−
skeleton/common/common.cabal+4
@@ -6,6 +6,10 @@ build-type: Simple
66library
77 hs-source-dirs: src
88 build-depends: base
9+ , obelisk-route
10+ , mtl
11+ , text
912
1013 exposed-modules:
1114 Common.Api
15+ Common.Route
skeleton/common/src/Common/Api.hs+1−1
@@ -1,4 +1,4 @@
11module Common.Api where
22
33commonStuff :: String
4−commonStuff = "here is a common string"
4+commonStuff = "Here is a string defined in code common to the frontend and backend."
skeleton/common/src/Common/Route.hsnew file+52
@@ -0,0 +1,52 @@
1+{-# LANGUAGE EmptyCase #-}
2+{-# LANGUAGE FlexibleContexts #-}
3+{-# LANGUAGE GADTs #-}
4+{-# LANGUAGE LambdaCase #-}
5+{-# LANGUAGE RankNTypes #-}
6+{-# LANGUAGE TemplateHaskell #-}
7+{-# LANGUAGE KindSignatures #-}
8+{-# LANGUAGE EmptyCase #-}
9+{-# LANGUAGE MultiParamTypeClasses #-}
10+{-# LANGUAGE FlexibleInstances #-}
11+{-# LANGUAGE OverloadedStrings #-}
12+
13+module Common.Route where
14+
15+{- -- You will probably want these imports for composing Encoders.
16+import Prelude hiding (id, (.))
17+import Control.Category
18+-}
19+
20+import Data.Text (Text)
21+import Data.Functor.Identity
22+import Data.Functor.Sum
23+
24+import Obelisk.Route
25+import Obelisk.Route.TH
26+
27+data BackendRoute :: * -> * where
28+ -- | Used to handle unparseable routes.
29+ BackendRoute_Missing :: BackendRoute ()
30+ -- You can define any routes that will be handled specially by the backend here.
31+ -- i.e. These do not serve the frontend, but do something different, such as serving static files.
32+
33+data FrontendRoute :: * -> * where
34+ FrontendRoute_Main :: FrontendRoute ()
35+ -- This type is used to define frontend routes, i.e. ones for which the backend will serve the frontend.
36+
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+
48+
49+concat <$> mapM deriveRouteComponent
50+ [ ''BackendRoute
51+ , ''FrontendRoute
52+ ]
skeleton/devel/Devel.hsdeleted−10
@@ -1,10 +0,0 @@
1−module Devel where
2−
3−import Obelisk.Run
4−
5−import Backend
6−import Frontend
7−
8−main :: Int -> IO ()
9−main port = run port backend frontend
10−
skeleton/frontend/frontend.cabal+5−2
@@ -7,6 +7,8 @@ library
77 hs-source-dirs: src
88 build-depends: base
99 , common
10+ , obelisk-frontend
11+ , obelisk-route
1012 , reflex-dom
1113 , obelisk-generated-static
1214 , text
@@ -16,12 +18,13 @@ library
1618
1719executable frontend
1820 main-is: main.hs
19− hs-source-dirs: src
21+ hs-source-dirs: src-bin
2022 build-depends: base
2123 , common
24+ , obelisk-frontend
25+ , obelisk-route
2226 , reflex-dom
2327 , obelisk-generated-static
24− , text
2528 , frontend
2629 --TODO: Make these ghc-options optional
2730 ghc-options: -threaded
skeleton/frontend/src-bin/main.hsnew file+10
@@ -0,0 +1,10 @@
1+import Frontend
2+import Common.Route
3+import Obelisk.Frontend
4+import Obelisk.Route.Frontend
5+import Reflex.Dom
6+
7+main :: IO ()
8+main = do
9+ let Right validFullEncoder = checkEncoder backendRouteEncoder
10+ run $ runFrontend validFullEncoder frontend
skeleton/frontend/src/Frontend.hs+9−5
@@ -4,16 +4,20 @@
44module Frontend where
55
66import qualified Data.Text as T
7+import Obelisk.Frontend
8+import Obelisk.Route
79import Reflex.Dom.Core
810
911import Common.Api
12+import Common.Route
1013import Obelisk.Generated.Static
1114
12−frontend :: (StaticWidget x (), Widget x ())
13−frontend = (head', body)
14− where
15− head' = el "title" $ text "Obelisk Minimal Example"
16− body = do
15+
16+frontend :: Frontend (R FrontendRoute)
17+frontend = Frontend
18+ { _frontend_head = el "title" $ text "Obelisk Minimal Example"
19+ , _frontend_body = do
1720 text "Welcome to Obelisk!"
1821 el "p" $ text $ T.pack commonStuff
1922 elAttr "img" ("src" =: static @"obelisk.jpg") blank
23+ }
skeleton/frontend/src/main.hsdeleted−5
@@ -1,5 +0,0 @@
1−import Reflex.Dom
2−import Frontend
3−
4−main :: IO ()
5−main = mainWidget $ snd frontend