on master help sign in
Merge branch 'master' into devTools
commit58a0bffa4ab71e2152c47a34459f5015d1349c1b authorRobert Klotzner <eskimor@users.noreply.github.com> dateThu 18 Oct 2018, 15:55 (8 years ago) parent24105ec6983037
55 files changed+2303−411
README.md+2−2 default.nix+156−135 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+15−5 lib/backend/obelisk-backend.cabal+11−1 lib/backend/src/Obelisk/Backend.hs+130−24 lib/cliapp/src/Obelisk/CliApp.hs+4−1 lib/cliapp/src/Obelisk/CliApp/Logging.hs+28−18 lib/cliapp/src/Obelisk/CliApp/Process.hs+13−4 lib/cliapp/src/Obelisk/CliApp/Types.hs+79−15 lib/command/obelisk-command.cabal+1 lib/command/src/Obelisk/App.hs+26−10 lib/command/src/Obelisk/Command.hs+22−8 lib/command/src/Obelisk/Command/Deploy.hs+2−2 lib/command/src/Obelisk/Command/Nix.hs+4 lib/command/src/Obelisk/Command/Project.hs+7−4 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+3−1 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+164 lib/migration/src/Obelisk/Migration.hs+4−3 lib/route/obelisk-route.cabal+36 lib/route/src/Obelisk/Route.hs+818 lib/route/src/Obelisk/Route/Frontend.hs+346 lib/route/src/Obelisk/Route/TH.hs+18 lib/run/obelisk-run.cabal+13−1 lib/run/src/Obelisk/Run.hs+79−36 lib/selftest/src/Obelisk/SelfTest.hs+7−1 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+7−4 skeleton/frontend/src-bin/main.hs+10 skeleton/frontend/src/Frontend.hs+10−6 skeleton/frontend/src/main.hs−5
README.md+2−2
@@ -152,7 +152,7 @@ cd ~/code/myapp-deploy
152152ob deploy push
153153```
154154
155−`ob deploy push` will locally build your app and then transfer it, along with all the Nix package dependencies, via ssh to the EC2 instance.
155+`ob deploy push` will locally build your app and then transfer it, along with all the Nix package dependencies, via ssh to the EC2 instance. The backend will live in `/var/lib/backend`.
156156
157157At this point you are done. Your app will be accessible at `${ROUTE}`.
158158
@@ -266,7 +266,7 @@ First, if you do not already have a keystore, create it as follows (for more inf
266266nix-shell -p androidenv.platformTools --run "keytool -genkey -v -keystore myandroidkey.jks -keyalg RSA -keysize 2048 -validity 10000 -alias myandroidalias"
267267```
268268
269−(Besure to give an appropriate keystore filename and key alias string above.)
269+(Be sure to give an appropriate keystore filename and key alias string above.)
270270
271271The `keytool` command will ask you for some details, including a keystore password and a key password (we will use these passwords further below). It will now have created a `myandroidkey.jks` file under the current directory. Move that to somewhere safe, and note down its full path.
272272
default.nix+156−135
@@ -3,10 +3,10 @@
33, iosSdkVersion ? "10.2"
44}:
55let
6− getReflexPlatform = sys: import ./dep/reflex-platform { inherit iosSdkVersion; system = sys; };
6+ getReflexPlatform = sys: import ./dep/reflex-platform { inherit iosSdkVersion; system = sys; enableLibraryProfiling = profiling; };
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
@@ -21,7 +21,6 @@ let
2121 git
2222 gitAndTools.hub
2323 nix-prefetch-git
24− nixStable
2524 openssh
2625 ];
2726
@@ -49,27 +48,8 @@ let
4948 # The haskell environment used to build Obelisk itself, e.g. the 'ob' command
5049 ghcObelisk = reflex-platform.ghc.override {
5150 overrides = composeExtensions defaultHaskellOverrides (self: super: {
52− mkDerivation = args: super.mkDerivation (args // {
53− enableLibraryProfiling = profiling;
54− });
55−
56− #TODO: Eliminate this when https://github.com/phadej/github/pull/307 makes its way to reflex-platform
57− github = overrideCabal super.github (drv: {
58− src = pkgs.fetchFromGitHub {
59− owner = "ryantrinkle";
60− repo = "github";
61− rev = "8f543cdc07876bfb7b924d3722e3dbc1df4b02ca";
62− sha256 = "0vcnx9cxqd821kmjx1r4cvj95zs742qm1pwqnb52vw3djplbqd86";
63− };
64− sha256 = null;
65− revision = null;
66− editedCabalFile = null;
67− });
68−
6951 # Dynamic linking with split objects dramatically increases startup time (about 0.5 seconds on a decent machine with SSD)
7052 obelisk-command = addOptparseApplicativeCompletionScripts "ob" (justStaticExecutables' super.obelisk-command);
71−
72− optparse-applicative = self.callHackage "optparse-applicative" "0.14.0.0" {};
7353 });
7454 };
7555
@@ -77,32 +57,22 @@ let
7757 ghcObeliskEnvs = mapAttrs (n: v: reflex-platform.workOn ghcObelisk ghcObelisk.${v}) ghcObelisk;
7858
7959 fixUpstreamPkgs = self: super: {
80− heist = doJailbreak super.heist; #TODO: Move up to reflex-platform; create tests for r-p supported packages
81− modern-uri =
82− let src = pkgs.fetchFromGitHub {
83− owner = "mrkkrp";
84− repo = "modern-uri";
85− rev = "21064285deb284cb3328094c69c34f9f67919cc9";
86− sha256 = "0vddw8r9sb31h1fz1anzxrs9p3a3p8ygpxlj398z5j47wmr86cmi";
87− };
88− in (overrideCabal (self.callCabal2nix "modern-uri" src {}) (drv: {
89− doCheck = false;
90− postPatch = (drv.postPatch or "") + ''
91− substituteInPlace Text/URI/Types.hs \
92− --replace "instance Arbitrary (NonEmpty (RText 'PathPiece)) where" "" \
93− --replace " arbitrary = (:|) <$> arbitrary <*> arbitrary" ""
94− '';
95− })).override { megaparsec = super.megaparsec_6_1_1; };
96− algebraic-graphs =
97− let src = pkgs.fetchFromGitHub {
98− owner = "snowleopard";
99− repo = "alga";
100− rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
101− sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
102− };
103− in pkgs.haskell.lib.dontCheck (self.callCabal2nix "algebraic-graphs" src {});
104− network-transport = self.callHackage "network-transport" "0.5.2" {};
105− network-transport-tcp = self.callHackage "network-transport-tcp" "0.6.0" {};
60+ algebraic-graphs = pkgs.haskell.lib.doJailbreak
61+ (self.callCabal2nix "algebraic-graphs" (pkgs.fetchFromGitHub {
62+ owner = "snowleopard";
63+ repo = "alga";
64+ rev = "480a73137e9b38ad3f1bc2c628847953d2fb3e25";
65+ sha256 = "0dpwi5ffs88brl3lz51bwb004c6zm8ds8pkw1vzsg2a6aaiyhlzl";
66+ }) {});
67+
68+ # Need deriveSomeUniverse
69+ # PR: https://github.com/dmwit/universe/pull/32
70+ universe-template = self.callCabal2nix "universe-template" (pkgs.fetchFromGitHub {
71+ owner = "obsidiansystems";
72+ repo = "universe";
73+ rev = "5a2fc823caa4163411d7e41aa80e67cefb15944a";
74+ sha256 = "0ll2z0fh18z6x8jl8kbp7ldagwccz3wjmvrw1gw752z058n82yfa";
75+ } + /template) {};
10676 };
10777
10878 cleanSource = builtins.filterSource (name: _: let baseName = builtins.baseNameOf name; in !(
@@ -117,16 +87,33 @@ let
11787 obelisk-asset-serve-snap = self.callCabal2nix "obelisk-asset-serve-snap" (hackGet ./lib/asset + "/serve-snap") {};
11888 obelisk-backend = self.callCabal2nix "obelisk-backend" (cleanSource ./lib/backend) {};
11989 obelisk-cliapp = self.callCabal2nix "obelisk-cliapp" (cleanSource ./lib/cliapp) {};
120− obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).override { Cabal = super.Cabal_2_0_0_2; };
90+ obelisk-command = (self.callCabal2nix "obelisk-command" (cleanSource ./lib/command) {}).overrideAttrs
91+ (drv: {
92+ buildInputs = drv.buildInputs ++ [ pkgs.makeWrapper ];
93+ postInstall = ''
94+ ${drv.postInstall or ""}
95+ # Install migrations
96+ cp -r ${./migration} $out/migration;
97+ '';
98+ postFixup = ''
99+ ${drv.postFixup or ""}
100+ # Make `ob` reference its runtime dependencies.
101+ wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
102+ '';
103+ });
121104 obelisk-executable-config = executableConfig.haskellPackage self;
122− obelisk-executable-config-inject = executableConfig.platforms.web.inject self; # TODO handle platforms.{ios,android}
105+ obelisk-executable-config-inject = executableConfig.platforms.web.inject self;
106+ obelisk-frontend = self.callCabal2nix "obelisk-frontend" (cleanSource ./lib/frontend) {};
123107 obelisk-migration = self.callCabal2nix "obelisk-migration" (cleanSource ./lib/migration) {};
124108 obelisk-run = self.callCabal2nix "obelisk-run" (cleanSource ./lib/run) {};
109+ obelisk-route = self.callCabal2nix "obelisk-route" (cleanSource ./lib/route) {};
125110 obelisk-selftest = self.callCabal2nix "obelisk-selftest" (cleanSource ./lib/selftest) {};
126111 obelisk-snap = self.callCabal2nix "obelisk-snap" (cleanSource ./lib/snap) {};
127112 obelisk-snap-extras = self.callCabal2nix "obelisk-snap-extras" (cleanSource ./lib/snap-extras) {};
128113 };
129114
115+ inherit (import ./lib/asset/assets.nix { inherit nixpkgs; }) mkAssets;
116+
130117 defaultHaskellOverrides = composeExtensions fixUpstreamPkgs addLibs;
131118
132119in
@@ -138,16 +125,9 @@ rec {
138125 path = reflex-platform.filterGit ./.;
139126 obelisk = ghcObelisk;
140127 obeliskEnvs = ghcObeliskEnvs;
141− commandWithMigration = ghcObelisk.obelisk-command.overrideAttrs (drv: {
142− postInstall = (drv.postInstall or "") +
143− ''cp -r ${./migration} $out/migration;'';
144− });
145− command = pkgs.runCommand commandWithMigration.name { nativeBuildInputs = [pkgs.makeWrapper]; } ''
146− mkdir -p "$out/bin"
147− ln -s '${commandWithMigration}/bin/ob' "$out/bin/ob"
148− wrapProgram "$out"/bin/ob --prefix PATH : ${pkgs.lib.makeBinPath (commandRuntimeDeps pkgs)}
149− '';
150− commandEnv = ghcObelisk.obelisk-command.env;
128+
129+ command = ghcObelisk.obelisk-command;
130+
151131 shell = pinBuildInputs "obelisk-shell" ([command] ++ commandRuntimeDeps pkgs) [];
152132
153133 selftest = pkgs.writeScript "selftest" ''
@@ -167,7 +147,7 @@ rec {
167147 nullIfAbsent = p: if pathExists p then p else null;
168148 haskellOverrides = addLibs;
169149 #TODO: Avoid copying files within the nix store. Right now, obelisk-asset-manifest-generate copies files into a big blob so that the android/ios static assets can be imported from there; instead, we should get everything lined up right before turning it into an APK, so that copies, if necessary, only exist temporarily.
170− processAssets = { src, packageName ? "static", moduleName ? "Static" }: pkgs.runCommand "asset-manifest" {
150+ processAssets = { src, packageName ? "obelisk-generated-static", moduleName ? "Obelisk.Generated.Static" }: pkgs.runCommand "asset-manifest" {
171151 inherit src;
172152 outputs = [ "out" "haskellManifest" "symlinked" ];
173153 buildInputs = [
@@ -180,95 +160,123 @@ rec {
180160 '';
181161
182162 compressedJs = frontend: pkgs.runCommand "compressedJs" { buildInputs = [ pkgs.closurecompiler ]; } ''
183− mkdir $out
184− cd $out
185− ln -s "${justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
186− 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
187− echo "//# sourceMappingURL=all.js.map" >> all.js
163+ mkdir $out
164+ cd $out
165+ ln -s "${justStaticExecutables frontend}/bin/frontend.jsexe/all.js" all.unminified.js
166+ 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
167+ echo "//# sourceMappingURL=all.js.map" >> all.js
188168 '';
189169
170+ serverModules = {
171+ mkBaseEc2 = { hostName, routeHost, enableHttps, adminEmail, ... }: {...}: {
172+ imports = [
173+ (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
174+ ];
175+ networking = {
176+ inherit hostName;
177+ firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
178+ };
179+ security.acme.certs = if enableHttps then {
180+ "${routeHost}".email = adminEmail;
181+ } else {};
182+ ec2.hvm = true;
183+ };
184+
185+ mkObeliskApp =
186+ { exe
187+ , routeHost
188+ , enableHttps
189+ , name ? "backend"
190+ , user ? name
191+ , group ? user
192+ , baseUrl ? "/"
193+ , internalPort ? 8000
194+ , backendArgs ? "--port=${toString internalPort}"
195+ , ...
196+ }: {...}: {
197+ services.nginx = {
198+ enable = true;
199+ virtualHosts."${routeHost}" = {
200+ enableACME = enableHttps;
201+ forceSSL = enableHttps;
202+ locations.${baseUrl} = {
203+ proxyPass = "http://localhost:" + toString internalPort;
204+ };
205+ };
206+ };
207+ systemd.services.${name} = {
208+ wantedBy = [ "multi-user.target" ];
209+ after = [ "network.target" ];
210+ restartIfChanged = true;
211+ script = ''
212+ ln -sft . '${exe}'/*
213+ mkdir -p log
214+ exec ./backend ${backendArgs} >>backend.out 2>>backend.err </dev/null
215+ '';
216+ serviceConfig = {
217+ User = user;
218+ KillMode = "process";
219+ WorkingDirectory = "~";
220+ Restart = "always";
221+ RestartSec = 5;
222+ };
223+ };
224+ users = {
225+ users.${user} = {
226+ description = "${user} service";
227+ home = "/var/lib/${user}";
228+ createHome = true;
229+ isSystemUser = true;
230+ group = group;
231+ };
232+ groups.${group} = {};
233+ };
234+ };
235+ };
236+
190237 serverExe = backend: frontend: assets: config:
191238 pkgs.runCommand "serverExe" {} ''
192239 mkdir $out
193240 set -eux
194− ln -s "${justStaticExecutables backend}"/bin/backend $out/backend
195− ln -s "${assets}" $out/static
196− ln -s "${config}" $out/config
197− ln -s ${compressedJs frontend} $out/frontend.jsexe
198− ''; #TODO: run frontend.jsexe through the asset processing pipeline
241+ ln -s "${justStaticExecutables backend}"/bin/* $out/
242+ ln -s "${mkAssets assets}" $out/static.assets
243+ cp -r ${config} $out/config
244+ ln -s ${mkAssets (compressedJs frontend)} $out/frontend.jsexe.assets
245+ '';
199246
200− server = { exe, hostName, adminEmail, routeHost, enableHttps }:
201− let system = "x86_64-linux";
202− nixos = import (pkgs.path + /nixos);
203− backendPort = 8000;
247+ server = { exe, hostName, adminEmail, routeHost, enableHttps, config }@args:
248+ let
249+ nixos = import (pkgs.path + /nixos);
204250 in nixos {
205− inherit system;
206− configuration = args: {
251+ system = "x86_64-linux";
252+ configuration = {
207253 imports = [
208− (pkgs.path + /nixos/modules/virtualisation/amazon-image.nix)
254+ (serverModules.mkBaseEc2 args)
255+ (serverModules.mkObeliskApp args)
209256 ];
210− networking = {
211− inherit hostName;
212− firewall.allowedTCPPorts = if enableHttps then [ 80 443 ] else [ 80 ];
213− };
214− services.nginx = {
215− enable = true;
216− virtualHosts."${routeHost}" = {
217− enableACME = enableHttps;
218− forceSSL = enableHttps;
219− locations."/" = {
220− proxyPass = "http://localhost:" + toString backendPort;
221− };
222− };
223− };
224− security.acme.certs = if enableHttps then {
225− "${routeHost}".email = adminEmail;
226− } else { };
227− systemd.services.backend = {
228− wantedBy = [ "multi-user.target" ];
229− after = [ "network.target" ];
230− restartIfChanged = true;
231− script = ''
232− ln -sft . "${exe}"/*
233− mkdir -p log
234− exec ./backend >>backend.out 2>>backend.err </dev/null
235− '';
236− serviceConfig = {
237− User = "backend";
238− KillMode = "process";
239− WorkingDirectory = "~";
240− Restart = "always";
241− RestartSec = 5;
242− };
243− };
244− users.extraUsers.backend = {
245− description = "backend service";
246− home = "/var/lib/backend";
247− createHome = true;
248− isSystemUser = true;
249− group = "backend";
250− };
251− ec2.hvm = true;
252257 };
253258 };
254259
255260
256261 # An Obelisk project is a reflex-platform project with a predefined layout and role for each component
257262 project = base: projectDefinition:
258− let assets = processAssets { src = base + "/static"; };
259− configPath = base + "/config";
263+ let configPath = base + "/config";
264+ static = base + "/static";
265+ processedStatic = processAssets { src = static; };
260266 projectOut = sys: (getReflexPlatform sys).project (args@{ nixpkgs, ... }:
261267 let mkProject = { android ? null #TODO: Better error when missing
262268 , ios ? null #TODO: Better error when missing
263269 , packages ? {}
264270 , overrides ? _: _: {}
271+ , tools ? _: []
272+ , shellToolOverrides ? _: _: {}
273+ , withHoogle ? false # Setting this to `true` makes shell reloading far slower
265274 }:
266275 let frontendName = "frontend";
267276 backendName = "backend";
268277 commonName = "common";
269− staticName = "static";
278+ staticName = "obelisk-generated-static";
270279 staticPath = base + "/static";
271− assets = processAssets { src = base + "/static"; };
272280 # The packages whose names and roles are defined by this package
273281 predefinedPackages = filterAttrs (_: x: x != null) {
274282 ${frontendName} = nullIfAbsent (base + "/frontend");
@@ -277,14 +285,25 @@ rec {
277285 };
278286 combinedPackages = predefinedPackages // packages;
279287 projectOverrides = self: super: {
280− ${staticName} = dontHaddock (self.callCabal2nix "static" assets.haskellManifest {});
288+ ${staticName} = dontHaddock (self.callCabal2nix "static" processedStatic.haskellManifest {});
281289 ${backendName} = addBuildDepend super.${backendName} self.obelisk-run;
282290 };
283291 totalOverrides = composeExtensions (composeExtensions defaultHaskellOverrides projectOverrides) overrides;
292+ inherit (nixpkgs) lib;
293+ inherit (lib.strings) hasPrefix;
294+ privateConfigDirs = ["config/backend"];
295+ injectableConfig = builtins.filterSource (path: _:
296+ !(lib.lists.any (x: hasPrefix (toString base + "/" + toString x) (toString path)) privateConfigDirs)
297+ ) configPath;
284298 in {
299+ inherit shellToolOverrides tools withHoogle;
285300 overrides = totalOverrides;
286301 packages = combinedPackages;
287302 shells = {
303+ ghcSavedSplices = (filter (x: hasAttr x combinedPackages) [
304+ commonName
305+ frontendName
306+ ]);
288307 ghc = (filter (x: hasAttr x combinedPackages) [
289308 backendName
290309 commonName
@@ -295,29 +314,31 @@ rec {
295314 commonName
296315 ];
297316 };
298− withHoogle = false; # Setting this to `true` makes shell reloading far slower
299317 android = {
300318 ${if android == null then null else frontendName} = {
301319 executableName = "frontend";
302− ${if builtins.pathExists staticPath then "assets" else null} = assets.symlinked;
320+ ${if builtins.pathExists staticPath then "assets" else null} =
321+ executableConfig.platforms.android.inject injectableConfig processedStatic.symlinked;
303322 } // android;
304323 };
305324 ios = {
306325 ${if ios == null then null else frontendName} = {
307326 executableName = "frontend";
308− ${if builtins.pathExists staticPath then "staticSrc" else null} = assets.symlinked;
327+ ${if builtins.pathExists staticPath then "staticSrc" else null} =
328+ executableConfig.platforms.ios.inject injectableConfig processedStatic.symlinked;
309329 } // ios;
310330 };
311331 };
312332 in mkProject (projectDefinition args));
313− serverOn = sys:
314− serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend assets.symlinked configPath;
333+ serverOn = sys: config: serverExe (projectOut sys).ghc.backend (projectOut system).ghcjs.frontend static config;
315334 linuxExe = serverOn "x86_64-linux";
316335 in projectOut system // {
317− inherit linuxExe;
318− exe = serverOn system;
319− server = args@{ hostName, adminEmail, routeHost, enableHttps }:
320− server (args // { exe = linuxExe;});
336+ linuxExeConfigurable = linuxExe;
337+ linuxExe = linuxExe (base + "/config");
338+ exe = serverOn system (base + "/config") ;
339+ server = args@{ hostName, adminEmail, routeHost, enableHttps, config }: let
340+ injectableConfig = builtins.filterSource (path: _: !(hasPrefix (toString config + "/backend") (toString path))) config;
341+ in server (args // { exe = linuxExe injectableConfig; });
321342 obelisk = import (base + "/.obelisk/impl") {};
322343 };
323344 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": "a4d679629b52f80ecae857864591d79d902a5c5d",
6− "sha256": "1p34qc3ryar81jh01g4jm0svr05yri83vlswm2jbxiabigy47652"
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+15−5
@@ -10,6 +10,7 @@ module Obelisk.Asset.Promoted
1010import Obelisk.Asset.Gather
1111
1212import Data.Foldable
13+import qualified Data.List as L
1314import Language.Haskell.TH (runQ, pprint)
1415import Language.Haskell.TH.Syntax hiding (lift)
1516import GHC.TypeLits
@@ -18,7 +19,6 @@ import qualified Data.Sequence as Seq
1819import Control.Monad.Trans.Writer
1920import System.FilePath
2021import System.Directory
21−import Data.Monoid
2222import Data.Text (Text)
2323import qualified Data.Text as T
2424import qualified Data.Text.IO as T
@@ -32,11 +32,15 @@ data StaticConfig = StaticConfig
3232
3333writeStaticProject :: Map FilePath FilePath -> FilePath -> StaticConfig -> IO ()
3434writeStaticProject paths target cfg = do
35− createDirectoryIfMissing True $ target </> "src"
35+ createDirectoryIfMissing True target
3636 T.writeFile (target </> T.unpack (_staticConfig_packageName cfg) <.> "cabal") $ staticCabalFile cfg
3737 let modName = _staticConfig_moduleName cfg
38+ (modName', moduleDirPath) = case L.uncons (reverse $ T.splitOn "." modName) of
39+ Nothing -> error $ "writeStaticProject: invalid module name " <> T.unpack modName
40+ Just (name, parents) -> (name, target </> "src" </> T.unpack (T.intercalate "/" $ reverse parents))
41+ createDirectoryIfMissing True moduleDirPath
3842 modContents <- staticModuleFile modName paths
39− T.writeFile (target </> "src" </> T.unpack modName <.> "hs") modContents
43+ T.writeFile (moduleDirPath </> T.unpack modName' <.> "hs") modContents
4044
4145staticCabalFile :: StaticConfig -> Text
4246staticCabalFile cfg = T.unlines
@@ -63,11 +67,17 @@ staticModuleFile moduleName paths = do
6367 , "{-# LANGUAGE FlexibleInstances #-}"
6468 , "{-# LANGUAGE KindSignatures #-}"
6569 , "{-# LANGUAGE OverloadedStrings #-}"
70+ , "{-# LANGUAGE ScopedTypeVariables #-}"
71+ , "{-# LANGUAGE TypeApplications #-}"
6672 , "module " <> moduleName <> " where"
6773 , ""
6874 , "import qualified GHC.Types"
69− , "import Data.Text ()"
75+ , "import Data.Text (Text)"
7076 , "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
7181 , ""
7282 , T.pack $ pprint decs
7383 ]
@@ -96,7 +106,7 @@ staticClass :: WriterT (Seq Dec) Q StaticContext
96106staticClass = do
97107 let n x = Name (OccName x) NameS
98108 className = n "StaticFile"
99− methodName = n "static"
109+ methodName = n "hashedPath"
100110 cls = ClassD [] className [KindedTV (n "s") (ConT ''Symbol)] [] [SigD methodName (ConT ''Text)]
101111 tell $ Seq.singleton cls
102112 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+4−1
@@ -6,12 +6,15 @@
66module Obelisk.CliApp
77 (
88 -- .Types
9− Cli
9+ CliLog
10+ , CliThrow
11+ , Cli
1012 , CliT(..)
1113 , runCli
1214 , CliConfig
1315 , HasCliConfig
1416 , getCliConfig
17+ , Output
1518
1619 -- .Spinner
1720 , withSpinner
lib/cliapp/src/Obelisk/CliApp/Logging.hs+28−18
@@ -7,12 +7,29 @@
77{-# LANGUAGE ScopedTypeVariables #-}
88-- | Provides a logging handler that facilitates safe ouputting to terminal using MVar based locking.
99-- | Spinner.hs and Process.hs work on this guarantee.
10−module Obelisk.CliApp.Logging where
10+module Obelisk.CliApp.Logging
11+ ( newCliConfig
12+ , runCli
13+ , verboseLogLevel
14+ , isOverwrite
15+ , getSeverity
16+ , getLogLevel
17+ , setLogLevel
18+ , putLog
19+ , putLogRaw
20+ , failWith
21+ , withExitFailMessage
22+ , writeLog
23+ , allowUserToMakeLoggingVerbose
24+ , getChars
25+ , fork
26+ ) where
1127
1228import Control.Concurrent (ThreadId, forkIO, killThread, threadDelay)
1329import Control.Concurrent.MVar (modifyMVar_, newMVar)
1430import Control.Monad (unless, void, when)
1531import Control.Monad.Catch (MonadCatch, MonadMask, bracket, catch, throwM)
32+import Control.Monad.Except (MonadError, throwError)
1633import Control.Monad.IO.Class (liftIO)
1734import Control.Monad.Log (Severity (..), WithSeverity (..), logMessage, runLoggingT)
1835import Control.Monad.Loops (iterateUntil)
@@ -25,7 +42,7 @@ import qualified Data.Text.IO as T
2542import System.Console.ANSI (Color (Red, Yellow), ColorIntensity (Vivid),
2643 ConsoleIntensity (FaintIntensity), ConsoleLayer (Foreground),
2744 SGR (SetColor, SetConsoleIntensity), clearLine)
28−import System.Exit (ExitCode (..), exitWith)
45+import System.Exit (ExitCode (..))
2946import System.IO (BufferMode (NoBuffering), hFlush, hReady, hSetBuffering, stderr, stdin, stdout)
3047
3148import qualified Obelisk.CliApp.TerminalString as TS
@@ -42,6 +59,7 @@ newCliConfig sev noColor noSpinner = do
4259runCli :: MonadIO m => CliConfig -> CliT m a -> m a
4360runCli c =
4461 flip runLoggingT (handleLog c)
62+ . unDieT
4563 . flip runReaderT c
4664 . unCliT
4765
@@ -59,17 +77,17 @@ getSeverity = \case
5977 Output_LogRaw (WithSeverity sev _) -> Just sev
6078 _ -> Nothing
6179
62−setLogLevel :: (MonadIO m, HasCliConfig m) => Severity -> m ()
63−setLogLevel sev = do
64− l <- _cliConfig_logLevel <$> getCliConfig
65− liftIO $ writeIORef l sev
66−
6780getLogLevel :: (MonadIO m, HasCliConfig m) => m Severity
6881getLogLevel = getLogLevel' =<< getCliConfig
6982
7083getLogLevel' :: MonadIO m => CliConfig -> m Severity
7184getLogLevel' = liftIO . readIORef . _cliConfig_logLevel
7285
86+setLogLevel :: (MonadIO m, HasCliConfig m) => Severity -> m ()
87+setLogLevel sev = do
88+ l <- _cliConfig_logLevel <$> getCliConfig
89+ liftIO $ writeIORef l sev
90+
7391handleLog :: MonadIO m => CliConfig -> Output -> m ()
7492handleLog conf output = do
7593 level <- getLogLevel' conf
@@ -107,21 +125,13 @@ handleLog' noColor output = do
107125 hFlush stdout
108126 return $ isOverwrite output
109127
110−-- | Log a message to the console.
111−--
112−-- Logs safely even if there are ongoing spinners.
113−putLog :: Cli m => Severity -> Text -> m ()
114−putLog sev = logMessage . Output_Log . WithSeverity sev
115−
116128-- | Like `putLog` but without the implicit newline added.
117129putLogRaw :: Cli m => Severity -> Text -> m ()
118130putLogRaw sev = logMessage . Output_LogRaw . WithSeverity sev
119131
120132-- | Like `putLog Alert` but also abrupts the program.
121−failWith :: (MonadIO m, Cli m) => Text -> m a
122−failWith s = do
123− putLog Alert s
124− liftIO $ exitWith $ ExitFailure 2
133+failWith :: MonadError Text m => Text -> m a
134+failWith = throwError
125135
126136-- | Intercept ExitFailure exceptions and log the given alert before exiting.
127137--
@@ -134,7 +144,7 @@ withExitFailMessage msg f = f `catch` \(e :: ExitCode) -> do
134144 throwM e
135145
136146-- | Write log to stdout, with colors (unless `noColor`)
137−writeLog:: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
147+writeLog :: (MonadIO m, MonadMask m) => Bool -> Bool -> WithSeverity Text -> m ()
138148writeLog withNewLine noColor (WithSeverity severity s)
139149 | noColor && severity <= Warning = liftIO $ putFn $ T.pack (show severity) <> ": " <> s
140150 | not noColor && severity <= Error = TS.putStrWithSGR errorColors h withNewLine s
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/cliapp/src/Obelisk/CliApp/Types.hs+79−15
@@ -7,21 +7,20 @@ module Obelisk.CliApp.Types where
77
88import Control.Concurrent.MVar (MVar)
99import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
10−import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..))
11−import Control.Monad.Reader (MonadIO, ReaderT (..), ask)
10+import Control.Monad.Log (LoggingT, MonadLog, Severity (..), WithSeverity (..), logMessage)
11+import Control.Monad.Reader (MonadIO, ReaderT (..), MonadReader, ask)
12+import Control.Monad.Writer (WriterT)
13+import Control.Monad.State (StateT)
14+import Control.Monad.Except (ExceptT, MonadError (..))
15+import Control.Monad.IO.Class (liftIO)
16+import Control.Monad.Trans (MonadTrans, lift)
1217import Data.IORef (IORef)
1318import Data.Text (Text)
19+import System.Exit (ExitCode (..), exitWith)
1420
1521import Obelisk.CliApp.TerminalString (TerminalString)
1622
17−data CliConfig = CliConfig
18− { _cliConfig_logLevel :: IORef Severity -- We are capable of changing the log level at runtime
19− , _cliConfig_noColor :: Bool -- Disallow coloured output
20− , _cliConfig_noSpinner :: Bool -- Disallow spinners
21− , _cliConfig_lock :: MVar Bool -- Whether the last message was an Overwrite output
22− , _cliConfig_tipDisplayed :: IORef Bool -- Whether the user tip (to make verbose) was already displayed
23− , _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString]) -- Stack of logs from nested spinners
24− }
23+--------------------------------------------------------------------------------
2524
2625data Output
2726 = Output_Log (WithSeverity Text) -- Regular logging message (with colors and newlines)
@@ -31,19 +30,84 @@ data Output
3130 | Output_ClearLine -- Clear the line
3231 deriving (Eq, Show, Ord)
3332
34−type Cli m = MonadLog Output m
33+type CliLog m = MonadLog Output m
3534
36−newtype CliT m a = CliT
37− { unCliT :: ReaderT CliConfig (LoggingT Output m) a
38− }
35+type CliThrow m = MonadError Text m
36+
37+-- TODO could remove for granularity
38+type Cli m = (CliLog m, CliThrow m)
39+
40+-- | Log a message to the console.
41+--
42+-- Logs safely even if there are ongoing spinners.
43+putLog :: Cli m => Severity -> Text -> m ()
44+putLog sev = logMessage . Output_Log . WithSeverity sev
45+
46+newtype DieT m a = DieT { unDieT :: LoggingT Output m a }
3947 deriving
4048 ( Functor, Applicative, Monad, MonadIO
49+ , MonadReader r
4150 , MonadThrow, MonadCatch, MonadMask
4251 , MonadLog Output
4352 )
4453
54+instance MonadTrans DieT where
55+ lift = DieT . lift
56+
57+-- TODO generalize to bigger error types
58+instance MonadIO m => MonadError Text (DieT m) where
59+ throwError s = do
60+ putLog Alert s
61+ liftIO $ exitWith $ ExitFailure 2
62+
63+ -- Cannot catch
64+ catchError m _ = m
65+
66+--------------------------------------------------------------------------------
67+
68+data CliConfig = CliConfig
69+ { -- | We are capable of changing the log level at runtime
70+ _cliConfig_logLevel :: IORef Severity
71+ , -- | Disallow coloured output
72+ _cliConfig_noColor :: Bool
73+ , -- | Disallow spinners
74+ _cliConfig_noSpinner :: Bool
75+ , -- | Whether the last message was an Overwrite output
76+ _cliConfig_lock :: MVar Bool
77+ , -- | Whether the user tip (to make verbose) was already displayed
78+ _cliConfig_tipDisplayed :: IORef Bool
79+ , -- | Stack of logs from nested spinners
80+ _cliConfig_spinnerStack :: IORef ([Bool], [TerminalString])
81+ }
82+
4583class Monad m => HasCliConfig m where
4684 getCliConfig :: m CliConfig
4785
86+instance HasCliConfig m => HasCliConfig (ReaderT r m) where
87+ getCliConfig = lift getCliConfig
88+
89+instance (Monoid w, HasCliConfig m) => HasCliConfig (WriterT w m) where
90+ getCliConfig = lift getCliConfig
91+
92+instance HasCliConfig m => HasCliConfig (StateT s m) where
93+ getCliConfig = lift getCliConfig
94+
95+instance HasCliConfig m => HasCliConfig (ExceptT e m) where
96+ getCliConfig = lift getCliConfig
97+
98+--------------------------------------------------------------------------------
99+
100+newtype CliT m a = CliT
101+ { unCliT :: ReaderT CliConfig (DieT m) a
102+ }
103+ deriving
104+ ( Functor, Applicative, Monad, MonadIO
105+ , MonadThrow, MonadCatch, MonadMask
106+ , MonadLog Output, MonadError Text, MonadReader CliConfig
107+ )
108+
109+instance MonadTrans CliT where
110+ lift = CliT . lift . lift
111+
48112instance Monad m => HasCliConfig (CliT m) where
49− getCliConfig = CliT ask
113+ getCliConfig = ask
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+26−10
@@ -9,10 +9,15 @@ module Obelisk.App where
99
1010import Control.Monad.Catch (MonadCatch, MonadMask, MonadThrow)
1111import Control.Monad.Reader (MonadIO, ReaderT (..), ask, runReaderT)
12−import Control.Monad.Trans.Class (lift)
12+import Control.Monad.Writer (WriterT)
13+import Control.Monad.State (StateT)
14+import Control.Monad.Except (ExceptT, MonadError)
15+import Control.Monad.Trans.Class (MonadTrans, lift)
16+import Data.Text (Text)
1317import System.Directory (XdgDirectory (XdgData), getXdgDirectory)
18+import Control.Monad.Log (MonadLog)
1419
15−import Obelisk.CliApp (Cli, CliConfig, CliT, HasCliConfig, getCliConfig, runCli)
20+import Obelisk.CliApp (CliConfig, CliLog, CliThrow, CliT (..), HasCliConfig, runCli, Output)
1621
1722newtype Obelisk = Obelisk
1823 { _obelisk_cliConfig :: CliConfig
@@ -23,18 +28,28 @@ newtype ObeliskT m a = ObeliskT
2328 }
2429 deriving
2530 ( Functor, Applicative, Monad, MonadIO, MonadThrow, MonadCatch, MonadMask
26− , HasObelisk, HasCliConfig)
31+ , MonadLog Output, MonadError Text, HasCliConfig)
2732
28−deriving instance Monad m => Cli (ObeliskT m)
33+instance MonadTrans ObeliskT where
34+ lift = ObeliskT . lift . lift
2935
30−class HasObelisk m where
36+class Monad m => HasObelisk m where
3137 getObelisk :: m Obelisk
3238
33−instance Monad m => HasObelisk (ReaderT Obelisk m) where
34− getObelisk = ask
39+instance Monad m => HasObelisk (ObeliskT m) where
40+ getObelisk = ObeliskT ask
3541
36−instance HasCliConfig m => HasCliConfig (ReaderT Obelisk m) where
37− getCliConfig = lift getCliConfig
42+instance HasObelisk m => HasObelisk (ReaderT r m) where
43+ getObelisk = lift getObelisk
44+
45+instance (Monoid w, HasObelisk m) => HasObelisk (WriterT w m) where
46+ getObelisk = lift getObelisk
47+
48+instance HasObelisk m => HasObelisk (StateT r m) where
49+ getObelisk = lift getObelisk
50+
51+instance HasObelisk m => HasObelisk (ExceptT e m) where
52+ getObelisk = lift getObelisk
3853
3954runObelisk :: MonadIO m => Obelisk -> ObeliskT m a -> m a
4055runObelisk c =
@@ -43,7 +58,8 @@ runObelisk c =
4358 . unObeliskT
4459
4560type MonadObelisk m =
46− ( Cli m
61+ ( CliLog m
62+ , CliThrow m
4763 , HasCliConfig m
4864 , HasObelisk m
4965 , MonadIO m
lib/command/src/Obelisk/Command.hs+22−8
@@ -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
@@ -87,12 +86,16 @@ initSource = foldl1 (<|>)
8786 , InitSource_Symlink <$> strOption (long "symlink" <> action "directory" <> metavar "PATH")
8887 ]
8988
89+initForce :: Parser Bool
90+initForce = switch (long "force" <> help "Allow ob init to overwrite files")
91+
9092data ObCommand
91− = ObCommand_Init InitSource
93+ = ObCommand_Init InitSource Bool
9294 | ObCommand_Deploy DeployCommand
9395 | ObCommand_Run
9496 | ObCommand_Thunk ThunkCommand
9597 | ObCommand_Repl
98+ | ObCommand_Watch
9699 | ObCommand_Upgrade (Maybe Text)
97100 | ObCommand_Internal ObInternal
98101 deriving Show
@@ -127,11 +130,12 @@ inNixShell' p = withProjectRoot "." $ \root -> do
127130obCommand :: ArgsConfig -> Parser ObCommand
128131obCommand cfg = hsubparser
129132 (mconcat
130− [ command "init" $ info (ObCommand_Init <$> initSource) $ progDesc "Initialize an Obelisk project"
133+ [ command "init" $ info (ObCommand_Init <$> initSource <*> initForce) $ progDesc "Initialize an Obelisk project"
131134 , command "deploy" $ info (ObCommand_Deploy <$> deployCommand cfg) $ progDesc "Prepare a deployment for an Obelisk project"
132135 , command "run" $ info (pure ObCommand_Run) $ progDesc "Run current project in development mode"
133136 , command "thunk" $ info (ObCommand_Thunk <$> thunkCommand) $ progDesc "Manipulate thunk directories"
134137 , command "repl" $ info (pure ObCommand_Repl) $ progDesc "Open an interactive interpreter"
138+ , command "watch" $ info (pure ObCommand_Watch) $ progDesc "Watch current project for errors and warnings"
135139 , 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"
136140 ])
137141 <|> subparser
@@ -274,7 +278,14 @@ mkObeliskConfig = do
274278 isTerm <- hIsTerminalDevice stdout
275279 -- Running in bash/fish/zsh completion
276280 inShellCompletion <- liftIO $ isInfixOf "completion" . unwords <$> getArgs
277− return $ isTerm && not inShellCompletion
281+
282+ -- Respect the user’s TERM environment variable. Dumb terminals
283+ -- like Eshell cannot handle lots of control sequences that the
284+ -- spinner uses.
285+ termEnv <- lookupEnv "TERM"
286+ let isDumb = termEnv == Just "dumb"
287+
288+ return $ isTerm && not inShellCompletion && not isDumb
278289
279290-- | For use from development obelisk repls
280291--
@@ -361,7 +372,7 @@ parseHandoff as' = case hasNoHandoff as' of
361372
362373ob :: MonadObelisk m => ObCommand -> m ()
363374ob = \case
364− ObCommand_Init source -> initProject source
375+ ObCommand_Init source force -> initProject source force
365376 ObCommand_Deploy dc -> case dc of
366377 DeployCommand_Init deployOpts -> withProjectRoot "." $ \root -> do
367378 let deployDir = _deployInitOpts_outputDir deployOpts
@@ -387,9 +398,11 @@ ob = \case
387398 enableHttps = _deployInitOpts_enableHttps deployOpts
388399 deployInit thunkPtr (root </> "config") deployDir
389400 sshKeyPath hostname route adminEmail enableHttps
390− DeployCommand_Push remoteBuilder -> deployPush "." $ case remoteBuilder of
391− Nothing -> pure []
392− Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
401+ DeployCommand_Push remoteBuilder -> do
402+ deployPath <- liftIO $ canonicalizePath "."
403+ deployPush deployPath $ case remoteBuilder of
404+ Nothing -> pure []
405+ Just RemoteBuilder_ObeliskVM -> (:[]) <$> VmBuilder.getNixBuildersArg
393406 DeployCommand_Update -> deployUpdate "."
394407 DeployCommand_Test Android -> deployMobile "android" []
395408 DeployCommand_Test (IOS teamID) -> deployMobile "ios" [teamID]
@@ -400,6 +413,7 @@ ob = \case
400413 ThunkCommand_Unpack thunks -> mapM_ unpackThunk thunks
401414 ThunkCommand_Pack thunks -> forM_ thunks $ \(ThunkPackOpts dir upstream) -> packThunk dir (T.pack upstream)
402415 ObCommand_Repl -> runRepl
416+ ObCommand_Watch -> inNixShell' $ static runWatch
403417 ObCommand_Upgrade branch -> do
404418 upgradeObelisk "." branch
405419 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+7−4
@@ -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
@@ -67,10 +66,14 @@ toImplDir :: FilePath -> FilePath
6766toImplDir p = toObeliskDir p </> "impl"
6867
6968-- | Create a new project rooted in the current directory
70−initProject :: MonadObelisk m => InitSource -> m ()
71−initProject source = withSystemTempDirectory "ob-init" $ \tmpDir -> do
69+initProject :: MonadObelisk m => InitSource -> Bool -> m ()
70+initProject source force = withSystemTempDirectory "ob-init" $ \tmpDir -> do
7271 let implDir = toImplDir tmpDir
7372 obDir = toObeliskDir tmpDir
73+ liftIO (listDirectory ".") >>= \case
74+ [] -> pure ()
75+ _ | force -> putLog Warning "Initializing in non-empty directory"
76+ | otherwise -> failWith "ob init requires an empty directory. Use the flag --force to init anyway, potentially overwriting files."
7477 skeleton <- withSpinner "Setting up obelisk" $ do
7578 liftIO $ createDirectory obDir
7679 case source of
@@ -111,7 +114,7 @@ initProject source = withSystemTempDirectory "ob-init" $ \tmpDir -> do
111114--TODO: Allow the user to ignore our security concerns
112115-- | Find the Obelisk implementation for the project at the given path
113116findProjectObeliskCommand :: MonadObelisk m => FilePath -> m (Maybe FilePath)
114−findProjectObeliskCommand target = withSpinnerNoTrail "Locating project obelisk" $ do
117+findProjectObeliskCommand target = do
115118 myUid <- liftIO getRealUserID
116119 targetStat <- liftIO $ getFileStatus target
117120 (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 -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+3−1
@@ -9,8 +9,10 @@ library
99 build-depends:
1010 base,
1111 bytestring,
12+ directory,
1213 filepath,
13− reflex-dom,
14+ 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
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+164
@@ -0,0 +1,164 @@
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 GHCJS.DOM hiding (bracket, catch)
35+import GHCJS.DOM.Document
36+import GHCJS.DOM.Node
37+import qualified GHCJS.DOM.Types as DOM
38+import Language.Javascript.JSaddle (JSM)
39+import Obelisk.Route.Frontend
40+import Reflex.Dom.Core
41+import Reflex.Host.Class
42+import qualified Reflex.TriggerEvent.Base as TriggerEvent
43+
44+makePrisms ''Sum
45+
46+type ObeliskWidget t x route m =
47+ ( DomBuilder t m
48+ , MonadFix m
49+ , MonadHold t m
50+ , MonadSample t (Performable m)
51+ , MonadReflexCreateTrigger t m
52+ , PostBuild t m
53+ , PerformEvent t m
54+ , MonadIO m
55+ , MonadIO (Performable m)
56+ , TriggerEvent t m
57+ , HasDocument m
58+ , MonadRef m
59+ , Ref m ~ Ref IO
60+ , MonadRef (Performable m)
61+ , Ref (Performable m) ~ Ref IO
62+ , MonadFix (Performable m)
63+ , PrimMonad m
64+ , Prerender x m
65+ , SetRoute t route m
66+ )
67+
68+data Frontend route = Frontend
69+ { _frontend_head :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
70+ , _frontend_body :: !(forall t m x. ObeliskWidget t x route m => RoutedT t route m ())
71+ }
72+
73+type Widget' x = ImmediateDomBuilderT DomTimeline (DomCoreWidget x)
74+
75+-- | A widget that isn't attached to any particular part of the DOM hierarchy
76+type FloatingWidget x = TriggerEventT DomTimeline (DomCoreWidget x)
77+
78+type DomCoreWidget x = PostBuildT DomTimeline (WithJSContextSingleton x (PerformEventT DomTimeline DomHost))
79+
80+--TODO: Rename
81+{-# INLINABLE attachWidget''' #-}
82+attachWidget'''
83+ :: (EventChannel -> PerformEventT DomTimeline (SpiderHost Global) (IORef (Maybe (EventTrigger DomTimeline ()))))
84+ -> IO ()
85+attachWidget''' w = runDomHost $ do
86+ events <- liftIO newChan
87+ (postBuildTriggerRef, fc@(FireCommand fire)) <- hostPerformEventT $ w events
88+ mPostBuildTrigger <- readRef postBuildTriggerRef
89+ forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
90+ liftIO $ processAsyncEvents events fc
91+
92+--TODO: This is a collection of random stuff; we should make it make some more sense and then upstream to reflex-dom-core
93+runWithHeadAndBody
94+ :: ( (forall c. Widget' () c -> FloatingWidget () c) -- "Append to head"
95+ -> (forall c. Widget' () c -> FloatingWidget () c) -- "Append to body"
96+ -> FloatingWidget () ()
97+ )
98+ -> JSM ()
99+runWithHeadAndBody app = withJSContextSingletonMono $ \jsSing -> do
100+ globalDoc <- currentDocumentUnchecked
101+ headFragment <- createDocumentFragment globalDoc
102+ bodyFragment <- createDocumentFragment globalDoc
103+ unreadyChildren <- liftIO $ newIORef 0
104+ let commit = do
105+ headElement <- getHeadUnchecked globalDoc
106+ bodyElement <- getBodyUnchecked globalDoc
107+ void $ inAnimationFrame' $ \_ -> do
108+ replaceElementContents headElement headFragment
109+ replaceElementContents bodyElement bodyFragment
110+ liftIO $ attachWidget''' $ \events -> flip runWithJSContextSingleton jsSing $ do
111+ (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
112+ let appendImmediateDom :: DOM.DocumentFragment -> Widget' () c -> FloatingWidget () c
113+ appendImmediateDom df w = do
114+ events' <- TriggerEvent.askEvents
115+ lift $ do
116+ doc <- getOwnerDocumentUnchecked df
117+ let builderEnv = ImmediateDomBuilderEnv
118+ { _immediateDomBuilderEnv_document = doc
119+ , _immediateDomBuilderEnv_parent = toNode df
120+ , _immediateDomBuilderEnv_unreadyChildren = unreadyChildren
121+ , _immediateDomBuilderEnv_commitAction = commit
122+ }
123+ runImmediateDomBuilderT w builderEnv events'
124+ flip runPostBuildT postBuild $ flip runTriggerEventT events $ app (appendImmediateDom headFragment) (appendImmediateDom bodyFragment)
125+ liftIO (readIORef unreadyChildren) >>= \case
126+ 0 -> DOM.liftJSM commit
127+ _ -> return ()
128+ return postBuildTriggerRef
129+
130+runFrontend :: forall backendRoute route. Encoder Identity Identity (R (Sum backendRoute (ObeliskRoute route))) PageName -> Frontend (R route) -> JSM ()
131+runFrontend validFullEncoder frontend = do
132+ let ve = validFullEncoder . hoistParse errorLeft (prismEncoder (rPrism $ _InR . _ObeliskRoute_App))
133+ errorLeft = \case
134+ Left _ -> error "runFrontend: Unexpected non-app ObeliskRoute reached the frontend. This shouldn't happen."
135+ Right x -> Identity x
136+ runMyRouteViewT
137+ :: ( TriggerEvent t m
138+ , PerformEvent t m
139+ , MonadHold t m
140+ , DOM.MonadJSM m
141+ , DOM.MonadJSM (Performable m)
142+ , MonadFix m
143+ , MonadFix (Performable m)
144+ )
145+ => RoutedT t (R route) (SetRouteT t (R route) m) a
146+ -> m a
147+ runMyRouteViewT = runRouteViewT ve
148+ runWithHeadAndBody $ \appendHead appendBody -> runMyRouteViewT $ do
149+ mapRoutedT (mapSetRouteT appendHead) $ _frontend_head frontend
150+ mapRoutedT (mapSetRouteT appendBody) $ _frontend_body frontend
151+
152+renderFrontendHtml
153+ :: (t ~ SpiderTimeline Global)
154+ => r
155+ -> RoutedT t r (SetRouteT t r' (PostBuildT Spider (StaticDomBuilderT Spider (PerformEventT Spider (SpiderHost Global))))) ()
156+ -> RoutedT t r (SetRouteT t r' (PostBuildT Spider (StaticDomBuilderT Spider (PerformEventT Spider (SpiderHost Global))))) ()
157+ -> IO ByteString
158+renderFrontendHtml route headWidget bodyWidget = do
159+ --TODO: We should probably have a "NullEventWriterT" or a frozen reflex timeline
160+ html <- fmap snd $ renderStatic $ fmap fst $ runSetRouteT $ flip runRoutedT (pure route) $
161+ el "html" $ do
162+ el "head" headWidget
163+ el "body" bodyWidget
164+ return $ "<!DOCTYPE html>" <> html
lib/migration/src/Obelisk/Migration.hs+4−3
@@ -17,6 +17,7 @@ import Data.Set (Set)
1717import qualified Data.Set as Set
1818import Data.Text (Text)
1919import qualified Data.Text as T
20+import qualified Data.Text.IO as T
2021import System.Directory
2122import System.FilePath
2223
@@ -46,10 +47,10 @@ data MigrationError
4647 deriving (Eq, Ord, Show)
4748
4849class Action action where
49− parseEdgeMeta :: String -> action
50+ parseEdgeMeta :: Text -> action
5051
5152instance Action Text where
52− parseEdgeMeta = T.pack
53+ parseEdgeMeta = id
5354
5455-- | Get the directed-acylic graph of migration
5556-- TODO: Consider calling getDag only when reading the graph for efficiency.
@@ -75,7 +76,7 @@ readGraph root name = doesDirectoryExist root >>= \case
7576 let f = root </> (T.unpack $ v1 <> "-" <> v2) </> T.unpack graph
7677 doesFileExist f >>= \case
7778 True -> do
78− c <- readFile f >>= pure . parseEdgeMeta
79+ c <- parseEdgeMeta <$> T.readFile f
7980 return $ Just c
8081 False -> do
8182 return Nothing
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+818
@@ -0,0 +1,818 @@
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+ , maybeEncoder
50+ , maybeToEitherEncoder
51+ , justEncoder
52+ , nothingEncoder
53+ , isoEncoder
54+ , wrappedEncoder
55+ , unwrappedEncoder
56+ , listToNonEmptyEncoder
57+ , prefixNonemptyTextEncoder
58+ , joinPairTextEncoder
59+ , toListMapEncoder
60+ , shadowEncoder
61+ , prismEncoder
62+ , rPrism
63+ , obeliskRouteEncoder
64+ , obeliskRouteSegment
65+ , pageNameEncoder
66+ , handleEncoder
67+ , ObeliskRoute (..)
68+ , _ObeliskRoute_App
69+ , _ObeliskRoute_Resource
70+ , ResourceRoute (..)
71+ , JSaddleWarpRoute (..)
72+ , jsaddleWarpRouteEncoder
73+ , IndexOnlyRoute (..)
74+ , indexOnlyRouteSegment
75+ , indexOnlyRouteEncoder
76+ , someSumEncoder
77+ , Void1
78+ , void1Encoder
79+ , pathSegmentsTextEncoder
80+ , queryParametersTextEncoder
81+ ) where
82+
83+import Prelude hiding ((.), id)
84+
85+import Control.Applicative
86+import Control.Category (Category (..))
87+import qualified Control.Categorical.Functor as Cat
88+import Control.Categorical.Bifunctor
89+import Control.Category.Associative
90+import Control.Category.Monoidal
91+import Control.Lens (Identity (..), Prism', makePrisms, itraverse, imap, prism, (^.), re, matching, (^?), _Just, _Nothing, Iso', from, view, Wrapped (..))
92+import Control.Monad.Except
93+import Data.Dependent.Sum (DSum (..))
94+import Data.Dependent.Map (DMap)
95+import qualified Data.Dependent.Map as DMap
96+import Data.Either.Validation (Validation (..))
97+import Data.Foldable
98+import Data.Functor.Sum
99+import Data.GADT.Compare
100+import Data.GADT.Compare.TH
101+import Data.GADT.Show
102+import Data.List.NonEmpty (NonEmpty (..))
103+import Data.Map (Map)
104+import qualified Data.Map as Map
105+import Data.Maybe
106+import Data.Set (Set)
107+import qualified Data.Set as Set
108+import Data.Some (Some)
109+import qualified Data.Some as Some
110+import Data.Some.Universe.Orphans ()
111+import Data.Text (Text)
112+import qualified Data.Text as T
113+import Data.Text.Encoding
114+import Data.Universe
115+import Network.HTTP.Types.URI
116+import Obelisk.Route.TH
117+import Text.Read (readMaybe)
118+
119+-- Design goals:
120+-- No start-up time on the frontend (not yet met)
121+-- Able to ensure that there aren't overlapping routes prior to deployment
122+-- Easy to write common types of parsers
123+-- Completeness checking
124+
125+-- Laws:
126+-- We statically know that all valid Routes can be turned into valid URIs
127+-- - This means we need to know whether there are any overlaps, e.g. with the static file namespace
128+
129+--TODO:
130+-- Backend:
131+-- Redirect the user to a canonical route
132+-- Pre-render
133+-- Frontend:
134+-- Intercept links that can be kept within the app
135+-- Fragments
136+
137+--------------------------------------------------------------------------------
138+-- Subroutes/paths
139+--------------------------------------------------------------------------------
140+
141+type R f = DSum f Identity --TODO: Better name
142+
143+--TODO: COMPLETE pragma
144+infixr 5 :/
145+pattern (:/) :: f a -> a -> R f
146+pattern a :/ b = a :=> Identity b
147+
148+mapSome :: (forall a. f a -> g a) -> Some f -> Some g
149+mapSome f (Some.This a) = Some.This $ f a
150+
151+--------------------------------------------------------------------------------
152+-- Encoder fundamentals
153+--------------------------------------------------------------------------------
154+
155+-- | This is the type of route encoder/decoders. It is parameterised over two monads: Firstly, the monad
156+-- used to check the validity of the encoder (i.e. that it is total), secondly the monad used for parsing
157+-- during the decode phase. The following two parameters are respectively the type of decoded data, and the
158+-- encoded type.
159+newtype Encoder check parse decoded encoded =
160+ Encoder { unEncoder :: check (EncoderImpl parse decoded encoded) }
161+
162+unsafeEncoder :: check (EncoderImpl parse decoded encoded) -> Encoder check parse decoded encoded
163+unsafeEncoder = Encoder
164+
165+-- | The internal type used to construct primitive 'Encoder' values.
166+-- Law:
167+-- forall p. _encoderImpl_decode ve . _encoderImpl_encode ve p == pure
168+-- 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.
169+data EncoderImpl parse decoded encoded = EncoderImpl
170+ { _encoderImpl_decode :: !(encoded -> parse decoded) -- Can fail; can lose information; must always succeed on outputs of `_encoderImpl_encode` and result in the original value
171+ , _encoderImpl_encode :: !(decoded -> encoded) -- Must be injective
172+ }
173+
174+-- | Once an 'Encoder' has been checked, so that its check monad has become 'Identity', and its parser is total
175+-- so that its parse monad is also 'Identity', it may be used to actually decode by applying this function.
176+decode :: Encoder Identity Identity decoded encoded -> encoded -> decoded
177+decode e x = runIdentity (tryDecode e x)
178+
179+-- | Once an 'Encoder' has been checked, so that its check monad has become 'Identity', even if the same is not true of the
180+-- parse monad, we may still attempt to decode with it in its parse monad.
181+tryDecode :: Encoder Identity parse decoded encoded -> encoded -> parse decoded
182+tryDecode (Encoder (Identity impl)) x = _encoderImpl_decode impl x
183+
184+-- | Similar to 'decode' above, once an encoder has been checked so that its check monad is Identity, it
185+-- can be used to actually encode by using this. Note that while there's no constraint on the parse monad here,
186+-- one should usually be applying decode and encode to the same 'Encoder'
187+encode :: Encoder Identity parse decoded encoded -> decoded -> encoded
188+encode (Encoder (Identity impl)) x = _encoderImpl_encode impl x
189+
190+-- | This is a primitive used to build encoders which can't fail to check. It should not be used unless one is
191+-- reasonably certain that the law given for 'EncoderImpl' above holds.
192+unsafeMkEncoder :: (Applicative check) => EncoderImpl parse decoded encoded -> Encoder check parse decoded encoded
193+unsafeMkEncoder impl = Encoder (pure impl)
194+
195+-- | Transform the check monad of an 'Encoder' by applying a natural transformation.
196+hoistCheck :: (forall t. check t -> check' t) -> Encoder check parse a b -> Encoder check' parse a b
197+hoistCheck f (Encoder x) = Encoder (f x)
198+
199+-- | Transform the parse monad of an 'Encoder' by applying a natural transformation.
200+hoistParse :: (Functor check)
201+ => (forall t. parse t -> parse' t) -> Encoder check parse a b -> Encoder check parse' a b
202+hoistParse f (Encoder x) = Encoder (fmap (\(EncoderImpl dec enc) -> EncoderImpl (f . dec) enc) x)
203+
204+-- | Check an 'Encoder', transforming it into one whose check monad is anything we want (usually Identity).
205+checkEncoder :: (Applicative check', Functor check)
206+ => Encoder check parse decoded encoded
207+ -> check (Encoder check' parse decoded encoded)
208+checkEncoder = fmap unsafeMkEncoder . unEncoder
209+
210+instance (Applicative check, Monad parse) => Category (Encoder check parse) where
211+ id = Encoder $ pure id
212+ Encoder f . Encoder g = Encoder $ liftA2 (.) f g
213+
214+instance Monad parse => Category (EncoderImpl parse) where
215+ id = EncoderImpl
216+ { _encoderImpl_decode = pure
217+ , _encoderImpl_encode = id
218+ }
219+ f . g = EncoderImpl
220+ { _encoderImpl_decode = _encoderImpl_decode g <=< _encoderImpl_decode f
221+ , _encoderImpl_encode = _encoderImpl_encode f . _encoderImpl_encode g
222+ }
223+
224+instance Monad parse => PFunctor (,) (EncoderImpl parse) (EncoderImpl parse) where
225+ first f = bimap f id
226+instance Monad parse => QFunctor (,) (EncoderImpl parse) (EncoderImpl parse) where
227+ second g = bimap id g
228+instance Monad parse => Bifunctor (,) (EncoderImpl parse) (EncoderImpl parse) (EncoderImpl parse) where
229+ bimap f g = EncoderImpl
230+ { _encoderImpl_encode = bimap (_encoderImpl_encode f) (_encoderImpl_encode g)
231+ , _encoderImpl_decode = \(a, b) -> liftA2 (,) (_encoderImpl_decode f a) (_encoderImpl_decode g b)
232+ }
233+
234+instance (Applicative check, Monad parse) => PFunctor (,) (Encoder check parse) (Encoder check parse) where
235+ first f = bimap f id
236+instance (Applicative check, Monad parse) => QFunctor (,) (Encoder check parse) (Encoder check parse) where
237+ second g = bimap id g
238+instance (Applicative check, Monad parse) => Bifunctor (,) (Encoder check parse) (Encoder check parse) (Encoder check parse) where
239+ bimap f g = Encoder $ liftA2 bimap (unEncoder f) (unEncoder g)
240+
241+instance (Traversable f, Monad parse) => Cat.Functor f (EncoderImpl parse) (EncoderImpl parse) where
242+ fmap ve = EncoderImpl
243+ { _encoderImpl_encode = fmap $ _encoderImpl_encode ve
244+ , _encoderImpl_decode = traverse $ _encoderImpl_decode ve
245+ }
246+
247+instance (Traversable f, Monad check, Monad parse) => Cat.Functor f (Encoder check parse) (Encoder check parse) where
248+ fmap e = Encoder $ do
249+ ve <- unEncoder e
250+ pure $ Cat.fmap ve
251+
252+instance Monad parse => Associative (EncoderImpl parse) (,) where
253+ associate = EncoderImpl
254+ { _encoderImpl_encode = associate
255+ , _encoderImpl_decode = pure . disassociate
256+ }
257+ disassociate = EncoderImpl
258+ { _encoderImpl_encode = disassociate
259+ , _encoderImpl_decode = pure . associate
260+ }
261+
262+instance Monad parse => Monoidal (EncoderImpl parse) (,) where
263+ type Id (EncoderImpl parse) (,) = ()
264+ idl = EncoderImpl
265+ { _encoderImpl_encode = idl
266+ , _encoderImpl_decode = pure . coidl
267+ }
268+ idr = EncoderImpl
269+ { _encoderImpl_encode = idr
270+ , _encoderImpl_decode = pure . coidr
271+ }
272+ coidl = EncoderImpl
273+ { _encoderImpl_encode = coidl
274+ , _encoderImpl_decode = pure . idl
275+ }
276+ coidr = EncoderImpl
277+ { _encoderImpl_encode = coidr
278+ , _encoderImpl_decode = pure . idr
279+ }
280+
281+instance (Applicative check, Monad parse) => Associative (Encoder check parse) (,) where
282+ associate = Encoder $ pure associate
283+ disassociate = Encoder $ pure disassociate
284+
285+instance (Applicative check, Monad parse) => Monoidal (Encoder check parse) (,) where
286+ type Id (Encoder check parse) (,) = ()
287+ idl = Encoder $ pure idl
288+ idr = Encoder $ pure idr
289+ coidl = Encoder $ pure coidl
290+ coidr = Encoder $ pure coidr
291+
292+--------------------------------------------------------------------------------
293+-- Specific instances of encoders
294+--------------------------------------------------------------------------------
295+
296+-- | Given a valid 'Iso' from lens, construct an 'Encoder'
297+isoEncoder :: (Applicative check, Applicative parse) => Iso' a b -> Encoder check parse a b
298+isoEncoder f = unsafeMkEncoder $ EncoderImpl
299+ { _encoderImpl_encode = view f
300+ , _encoderImpl_decode = pure . view (from f)
301+ }
302+
303+wrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse (Unwrapped a) a
304+wrappedEncoder = isoEncoder $ from _Wrapped'
305+
306+unwrappedEncoder :: (Wrapped a, Applicative check, Applicative parse) => Encoder check parse a (Unwrapped a)
307+unwrappedEncoder = isoEncoder _Wrapped'
308+
309+maybeToEitherEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Maybe a) (Either () a)
310+maybeToEitherEncoder = unsafeMkEncoder $ EncoderImpl
311+ { _encoderImpl_encode = \case
312+ Nothing -> Left ()
313+ Just a -> Right a
314+ , _encoderImpl_decode = pure . \case
315+ Left _ -> Nothing
316+ Right a -> Just a
317+ }
318+
319+maybeEncoder
320+ :: ( MonadError Text check
321+ , Show a
322+ , Show b
323+ , check ~ parse
324+ )
325+ => Encoder check parse () b
326+ -> Encoder check parse a b
327+ -> Encoder check parse (Maybe a) b
328+maybeEncoder f g = shadowEncoder f g . maybeToEitherEncoder
329+
330+-- | Encode a value by simply applying 'Just'
331+justEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a (Maybe a)
332+justEncoder = prismEncoder _Just
333+
334+-- |
335+nothingEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse () (Maybe a)
336+nothingEncoder = prismEncoder _Nothing
337+
338+someConstEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Const a)) a
339+someConstEncoder = unsafeMkEncoder $ EncoderImpl
340+ { _encoderImpl_encode = \(Some.This (Const a)) -> a
341+ , _encoderImpl_decode = pure . Some.This . Const
342+ }
343+
344+-- | WARNING: This is only safe if the Show and Read instances for 'a' are
345+-- inverses of each other
346+unsafeTshowEncoder :: (Show a, Read a, Applicative check, MonadError Text parse) => Encoder check parse a Text
347+unsafeTshowEncoder = unsafeMkEncoder $ EncoderImpl
348+ { _encoderImpl_encode = tshow
349+ , _encoderImpl_decode = \raw -> case readMaybe $ T.unpack raw of
350+ Nothing -> throwError $ "unsafeTshowEncoderImpl: couldn't decode " <> tshow raw
351+ Just parsed -> pure parsed
352+ }
353+
354+newtype EncoderFunc check parse p r = EncoderFunc { runEncoderImplFunc :: forall a. p a -> Encoder check parse a r }
355+
356+newtype Flip f a b = Flip { unFlip :: f b a }
357+
358+checkEnum1EncoderFunc
359+ :: forall check check' parse p r.
360+ ( Universe (Some p)
361+ , GCompare p
362+ , Monad check
363+ , Applicative check'
364+ )
365+ => (forall a. p a -> Encoder check parse a r)
366+ -> check (EncoderFunc check' parse p r)
367+checkEnum1EncoderFunc f = do
368+ (encoderImpls :: DMap p (Flip (EncoderImpl parse) r)) <- DMap.fromList <$>
369+ traverse (\(Some.This p) -> (p :=>) . Flip <$> unEncoder (f p)) universe
370+ pure $ EncoderFunc $ \p -> unsafeMkEncoder . unFlip $
371+ DMap.findWithDefault (error "checkEnum1EncoderFunc: EncoderImpl not found (should be impossible)") p encoderImpls
372+
373+-- | 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)`.
374+data SegmentResult check parse a =
375+ 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
376+ | 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.
377+
378+-- | Encode a dependent sum of type `(R p)` into a PageName (i.e. the path and query part of a URL) by using the
379+-- supplied function to decide how to encode the constructors of p using the SegmentResult type. It is important
380+-- that the number of values of type `(Some p)` be relatively small in order for checking to complete quickly.
381+pathComponentEncoder
382+ :: forall check parse p.
383+ ( Universe (Some p)
384+ , GShow p
385+ , GCompare p
386+ , MonadError Text check
387+ , MonadError Text parse )
388+ => (forall a. p a -> SegmentResult check parse a)
389+ -> Encoder check parse (R p) PageName
390+pathComponentEncoder f = Encoder $ do
391+ let extractEncoder = \case
392+ PathEnd e -> first (unitEncoder []) . coidl . e
393+ PathSegment _ e -> e
394+ extractPathSegment = \case
395+ PathEnd _ -> Nothing
396+ PathSegment t _ -> Just t
397+ EncoderFunc f' <- checkEnum1EncoderFunc (extractEncoder . f)
398+ unEncoder (pathComponentEncoderImpl (enum1Encoder (extractPathSegment . f)) f')
399+
400+pathComponentEncoderImpl :: forall check parse p. (Monad check, Monad parse)
401+ => (Encoder check parse (Some p) (Maybe Text))
402+ -> (forall a. p a -> Encoder Identity parse a PageName)
403+ -> Encoder check parse (R p) PageName
404+pathComponentEncoderImpl this rest =
405+ chainEncoder (lensEncoder (\(_, b) a -> (a, b)) Prelude.fst consEncoder) this rest
406+
407+--NOTE: Naming convention in this module is to always talk about things in the *encoding* direction, never in the *decoding* direction
408+
409+chainEncoder
410+ :: forall check parse p r b.
411+ ( Monad check
412+ , Monad parse
413+ )
414+ => Encoder check parse (b, r) r
415+ -> Encoder check parse (Some p) b
416+ -> (forall a. p a -> Encoder Identity parse a r)
417+ -> Encoder check parse (R p) r
418+chainEncoder cons this rest = Encoder $ do
419+ consValid <- unEncoder cons
420+ thisValid <- unEncoder this
421+ pure $ EncoderImpl
422+ { _encoderImpl_decode = \v -> do
423+ (here, following) <- _encoderImpl_decode consValid v
424+ Some.This r <- _encoderImpl_decode thisValid here
425+ (r :/) <$> _encoderImpl_decode (runIdentity . unEncoder $ rest r) following
426+ , _encoderImpl_encode = \(r :/ s) ->
427+ _encoderImpl_encode consValid
428+ ( _encoderImpl_encode thisValid $ Some.This r
429+ , _encoderImpl_encode (runIdentity . unEncoder $ rest r) s)
430+ }
431+
432+--TODO: Do this in terms of a lens instead
433+lensEncoder :: (Applicative check, Monad parse)
434+ => (b -> [a] -> b) -> (b -> [a]) -> Encoder check parse (c, [a]) [a] -> Encoder check parse (c, b) b
435+lensEncoder set get g = Encoder $ do
436+ gImpl <- unEncoder g
437+ pure $ EncoderImpl
438+ { _encoderImpl_encode = \(ma, b) -> set b $ _encoderImpl_encode gImpl (ma, get b)
439+ , _encoderImpl_decode = \b -> do
440+ (ma, la) <- _encoderImpl_decode gImpl $ get b
441+ pure (ma, set b la)
442+ }
443+
444+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
445+consEncoder = unsafeMkEncoder $ EncoderImpl
446+ { _encoderImpl_encode = \(h, t) -> maybeToList h <> t
447+ , _encoderImpl_decode = pure . \case
448+ [] -> (Nothing, [])
449+ h:t -> (Just h, t)
450+ }
451+
452+tshow :: Show a => a -> Text
453+tshow = T.pack . show
454+
455+shadowEncoder
456+ :: ( Universe a
457+ , MonadError Text check
458+ , Show a
459+ , Show b
460+ , Show c
461+ , check ~ parse --TODO: Get rid of this
462+ )
463+ => Encoder check parse a c -- ^ Overlaps; should have a small number of possible routes
464+ -> Encoder check parse b c -- ^ Gets overlapped
465+ -> Encoder check parse (Either a b) c
466+shadowEncoder f g = Encoder $ do
467+ vf <- unEncoder f
468+ vg <- unEncoder g
469+ let gCanParse c = catchError (Just <$> _encoderImpl_decode vg c) (\_ -> pure Nothing)
470+ overlaps <- fmap catMaybes $ forM universe $ \a -> do
471+ let c = _encoderImpl_encode vf a
472+ mb <- gCanParse c
473+ pure $ fmap (\b -> (a, b, c)) mb
474+ case overlaps of
475+ [] -> pure ()
476+ _ -> throwError $ "shadowEncoder: overlap detected: " <> T.unlines
477+ (flip fmap overlaps $ \(a, b, c) -> "first encoder encodes " <> tshow a <> " as " <> tshow c <> ", which second encoder decodes as " <> tshow b)
478+ pure $ EncoderImpl
479+ { _encoderImpl_encode = \case
480+ Left a -> _encoderImpl_encode vf a
481+ Right b -> _encoderImpl_encode vg b
482+ , _encoderImpl_decode = \c -> (Left <$> _encoderImpl_decode vf c) `catchError` \_ -> Right <$> _encoderImpl_decode vg c
483+ }
484+
485+enum1Encoder
486+ :: ( Universe (Some p)
487+ , GShow p
488+ , GCompare p
489+ , MonadError Text check
490+ , MonadError Text parse
491+ , Ord r
492+ , Show r
493+ )
494+ => (forall a. p a -> r) -> Encoder check parse (Some p) r
495+enum1Encoder f = enumEncoder $ \(Some.This p) -> f p
496+
497+-- | Encode an enumerable, bounded type. WARNING: Don't use this on types that
498+-- have a large number of values - it will use a lot of memory.
499+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
500+enumEncoder f = Encoder $ do
501+ let reversed = Map.fromListWith (<>) [ (f p, Set.singleton p) | p <- universe ]
502+ checkSingleton k vs = case Set.toList vs of
503+ [] -> error "enumEncoder: empty reverse mapping; should be impossible"
504+ [e] -> Success e
505+ _ -> Failure $ Map.singleton k vs
506+ showRedundant :: r -> Set p -> [Text]
507+ showRedundant k vs = (" " <> tshow k <> " can decode to any of:")
508+ : fmap ((" "<>) . tshow) (Set.toList vs)
509+ case itraverse checkSingleton reversed :: Validation (Map r (Set p)) (Map r p) of
510+ Failure ambiguousEntries -> throwError $ T.unlines $
511+ "enumEncoder: ambiguous encodings detected:" : concat (Map.elems $ imap showRedundant ambiguousEntries)
512+ Success m -> pure $ EncoderImpl
513+ { _encoderImpl_decode = \r -> case Map.lookup r m of
514+ Just a -> pure a
515+ Nothing -> throwError $ "enumEncoder: not recognized: " <> tshow r --TODO: Report this as a better type
516+ , _encoderImpl_encode = f
517+ }
518+
519+unitEncoder :: (Applicative check, MonadError Text parse, Show r, Eq r) => r -> Encoder check parse () r
520+unitEncoder expected = unsafeMkEncoder $ EncoderImpl
521+ { _encoderImpl_decode = \obtained ->
522+ if obtained == expected
523+ then pure ()
524+ else throwError $ "endEncoderImpl: expected " <> tshow expected <> ", got " <> tshow obtained
525+ , _encoderImpl_encode = \_ -> expected
526+ }
527+
528+singlePathSegmentEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse Text PageName
529+singlePathSegmentEncoder = pathOnlyEncoder . singletonListEncoder
530+
531+pathOnlyEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse [Text] PageName
532+pathOnlyEncoder = unsafeMkEncoder $ EncoderImpl
533+ { _encoderImpl_decode = \(path, query) ->
534+ if query == mempty
535+ then pure path
536+ else throwError "pathOnlyEncoderImpl: query was provided"
537+ , _encoderImpl_encode = \path -> (path, mempty)
538+ }
539+
540+singletonListEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse a [a]
541+singletonListEncoder = unsafeMkEncoder $ EncoderImpl
542+ { _encoderImpl_decode = \case
543+ [a] -> pure a
544+ l -> throwError $ "singletonListEncoderImpl: expected one item, got " <> tshow (length l)
545+ , _encoderImpl_encode = (:[])
546+ }
547+
548+splitTextNonEmpty :: Text -> Text -> NonEmpty Text
549+splitTextNonEmpty separator v = case T.splitOn separator v of
550+ [] -> error "splitTextNonEmpty: Data.Text.splitOn should never return an empty list"
551+ h : t -> h :| t
552+
553+--TODO: To know this is reversible, we must know that the separator isn't included anywhere in the input text
554+pathSegmentsTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse (NonEmpty Text) Text
555+pathSegmentsTextEncoder = unsafeMkEncoder $ EncoderImpl
556+ { _encoderImpl_encode = T.intercalate "/" . fmap (urlEncodeText False) . toList
557+ , _encoderImpl_decode = pure . fmap (urlDecodeText False) . splitTextNonEmpty "/"
558+ }
559+
560+queryParametersTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse [(Text, Maybe Text)] Text
561+queryParametersTextEncoder = Encoder $ pure $ EncoderImpl
562+ { _encoderImpl_encode = \case
563+ [] -> ""
564+ params -> T.intercalate "&" (fmap encodeParameter params)
565+ , _encoderImpl_decode = pure . \case
566+ "" -> []
567+ encoded ->
568+ let h :| t = splitTextNonEmpty "&" encoded
569+ in fmap decodeParameter $ h : t
570+ }
571+ where
572+ encodeParameter (k, mv) = urlEncodeText True k <> case mv of
573+ Nothing -> ""
574+ Just v -> "=" <> urlEncodeText True v
575+ decodeParameter t =
576+ let (k, eqV) = T.breakOn "=" t
577+ mv = T.stripPrefix "=" eqV
578+ in (urlDecodeText True k, urlDecodeText True <$> mv)
579+
580+urlEncodeText :: Bool -> Text -> Text
581+urlEncodeText q = decodeUtf8 . urlEncode q . encodeUtf8
582+
583+urlDecodeText :: Bool -> Text -> Text
584+urlDecodeText q = decodeUtf8 . urlDecode q . encodeUtf8
585+
586+listToNonEmptyEncoder :: (Applicative check, Applicative parse, Monoid a, Eq a) => Encoder check parse [a] (NonEmpty a)
587+listToNonEmptyEncoder = Encoder $ pure $ EncoderImpl
588+ { _encoderImpl_encode = \case
589+ [] -> mempty :| []
590+ h : t -> h :| t
591+ , _encoderImpl_decode = \(h :| t) -> pure $
592+ if h == mempty
593+ then []
594+ else h : t
595+ }
596+
597+prefixTextEncoder :: (Applicative check, MonadError Text parse) => Text -> Encoder check parse Text Text
598+prefixTextEncoder p = Encoder $ pure $ EncoderImpl
599+ { _encoderImpl_encode = mappend p
600+ , _encoderImpl_decode = \v -> case T.stripPrefix p v of
601+ Nothing -> throwError $ "prefixTextEncoder: wrong prefix; expected " <> tshow p <> ", got " <> tshow (T.take (T.length p) v)
602+ Just stripped -> pure stripped
603+ }
604+
605+prefixNonemptyTextEncoder :: (Applicative check, MonadError Text parse) => Text -> Encoder check parse Text Text
606+prefixNonemptyTextEncoder p = Encoder $ pure $ EncoderImpl
607+ { _encoderImpl_encode = \case
608+ "" -> ""
609+ v -> p <> v
610+ , _encoderImpl_decode = \case
611+ "" -> pure ""
612+ v -> case T.stripPrefix p v of
613+ Nothing -> throwError $ "prefixTextEncoder: wrong prefix; expected " <> tshow p
614+ Just stripped -> pure stripped
615+ }
616+
617+unpackTextEncoder :: (Applicative check, Applicative parse) => Encoder check parse Text String
618+unpackTextEncoder = Encoder $ pure $ EncoderImpl
619+ { _encoderImpl_encode = T.unpack
620+ , _encoderImpl_decode = pure . T.pack
621+ }
622+
623+toListMapEncoder :: (Applicative check, Applicative parse, Ord k) => Encoder check parse (Map k v) [(k, v)]
624+toListMapEncoder = Encoder $ pure $ EncoderImpl
625+ { _encoderImpl_encode = Map.toList
626+ , _encoderImpl_decode = pure . Map.fromList --TODO: Should we be stricter about repeated keys?
627+ }
628+
629+joinPairTextEncoder :: (MonadError Text check, MonadError Text parse) => Text -> Encoder check parse (Text, Text) Text
630+joinPairTextEncoder = Encoder . \case
631+ "" -> throwError "joinPairTextEncoder: empty separator"
632+ separator -> pure $ EncoderImpl
633+ { _encoderImpl_encode = \(k, v) -> k <> separator <> v
634+ , _encoderImpl_decode = \r ->
635+ let (kt, vt) = T.breakOn separator r
636+ in case vt of
637+ -- The separator was not found
638+ "" -> throwError $ "joinPairTextEncoder: separator not found; expected " <> tshow separator
639+ _ -> return (kt, T.drop (T.length separator) vt)
640+ }
641+
642+--TODO: Rewrite this by composing the given prism with a lens on the first element of the DSum
643+-- Or, more likely, the user can compose it themselves
644+rPrism :: forall f g. (forall a. Prism' (f a) (g a)) -> Prism' (R f) (R g)
645+rPrism p = prism (\(g :/ x) -> g ^. re p :/ x) (\(f :/ x) -> bimap (:/ x) (:/ x) $ matching p f)
646+
647+-- | An encoder that only works on the items available via the prism. An error will be thrown in the parse monad
648+-- if the prism doesn't match.
649+prismEncoder :: (Applicative check, MonadError Text parse) => Prism' b a -> Encoder check parse a b
650+prismEncoder p = unsafeMkEncoder $ EncoderImpl
651+ { _encoderImpl_encode = (^. re p)
652+ , _encoderImpl_decode = \r -> case r ^? p of
653+ Just a -> pure a
654+ Nothing -> throwError "prismEncoder: value is not present in the prism"
655+ }
656+
657+-- | A URL path and query string, in which trailing slashes don't matter in the path
658+-- and duplicate query parameters are not allowed. A final goal of encoders using this library
659+-- will frequently be to produce this.
660+type PageName = ([Text], Map Text (Maybe Text))
661+
662+-- | A path (separated by slashes), and a query string.
663+type PathQuery = (String, String)
664+
665+-- | Encode a PageName into a path and query string.
666+pageNameEncoder :: (Applicative check, MonadError Text parse) => Encoder check parse PageName PathQuery
667+pageNameEncoder = bimap
668+ (unpackTextEncoder . prefixTextEncoder "/" . pathSegmentsTextEncoder . listToNonEmptyEncoder)
669+ (unpackTextEncoder . prefixNonemptyTextEncoder "?" . queryParametersTextEncoder . toListMapEncoder)
670+
671+-- | Handle an error in parsing, for example, in order to redirect to a 404 page.
672+handleEncoder
673+ :: (Functor check)
674+ => (e -> a)
675+ -> Encoder check (Either e) a b
676+ -> Encoder check Identity a b
677+handleEncoder recover e = Encoder $ do
678+ i <- unEncoder e
679+ return $ i
680+ { _encoderImpl_decode = \a -> pure $ case _encoderImpl_decode i a of
681+ Right r -> r
682+ Left err -> recover err
683+ }
684+
685+--------------------------------------------------------------------------------
686+-- Actual obelisk route info
687+--------------------------------------------------------------------------------
688+
689+-- | A type which can represent Obelisk-specific resource routes, in addition to application specific routes which serve your
690+-- frontend.
691+data ObeliskRoute :: (* -> *) -> * -> * where
692+ -- 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
693+ ObeliskRoute_App :: f a -> ObeliskRoute f a
694+ ObeliskRoute_Resource :: ResourceRoute a -> ObeliskRoute f a
695+
696+instance Universe (Some f) => Universe (Some (ObeliskRoute f)) where
697+ universe = fmap (\(Some.This x) -> Some.This (ObeliskRoute_App x)) universe
698+ ++ fmap (\(Some.This x) -> Some.This (ObeliskRoute_Resource x)) universe
699+
700+instance GEq f => GEq (ObeliskRoute f) where
701+ geq (ObeliskRoute_App x) (ObeliskRoute_App y) = geq x y
702+ geq (ObeliskRoute_Resource x) (ObeliskRoute_Resource y) = geq x y
703+ geq _ _ = Nothing
704+
705+instance GCompare f => GCompare (ObeliskRoute f) where
706+ gcompare (ObeliskRoute_App x) (ObeliskRoute_App y) = gcompare x y
707+ gcompare (ObeliskRoute_Resource x) (ObeliskRoute_Resource y) = gcompare x y
708+ gcompare (ObeliskRoute_App _) (ObeliskRoute_Resource _) = GLT
709+ gcompare (ObeliskRoute_Resource _) (ObeliskRoute_App _) = GGT
710+
711+-- | A type representing the various resource routes served by Obelisk. These can in principle map to any physical routes you want,
712+-- but sane defaults are provided by 'resourceRouteSegment'
713+data ResourceRoute :: * -> * where
714+ 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")
715+ ResourceRoute_Ghcjs :: ResourceRoute [Text]
716+ ResourceRoute_JSaddleWarp :: ResourceRoute (R JSaddleWarpRoute)
717+
718+-- | If there are no additional backend routes in your app (i.e. ObeliskRoute gives you all the routes you need),
719+-- this constructs a suitable 'Encoder' to use for encoding routes to 'PageName's. If you do have additional backend routes,
720+-- you'll want to use 'pathComponentEncoder' yourself, applied to a function that will likely use obeliskRouteSegment in order to
721+-- handle the ObeliskRoute case (i.e. Obelisk resource routes and app frontend routes).
722+obeliskRouteEncoder :: forall check parse appRoute.
723+ ( Universe (Some (ObeliskRoute appRoute))
724+ , GCompare (ObeliskRoute appRoute)
725+ , GShow appRoute
726+ , MonadError Text check
727+ , check ~ parse --TODO: Get rid of this
728+ )
729+ => (forall a. appRoute a -> SegmentResult check parse a)
730+ -> Encoder check parse (R (ObeliskRoute appRoute)) PageName
731+obeliskRouteEncoder appRouteSegment = pathComponentEncoder $ \r ->
732+ obeliskRouteSegment r appRouteSegment
733+
734+-- | From a function which explains how app-specific frontend routes translate into segments, produce a function which does the
735+-- same for ObeliskRoute. This uses the given function for the 'ObeliskRoute_App' case, and 'resourceRouteSegment' for the
736+-- 'ObeliskRoute_Resource' case.
737+obeliskRouteSegment :: forall check parse appRoute a.
738+ (MonadError Text check, MonadError Text parse)
739+ => ObeliskRoute appRoute a
740+ -> (forall b. appRoute b -> SegmentResult check parse b)
741+ -> SegmentResult check parse a
742+obeliskRouteSegment r appRouteSegment = case r of
743+ ObeliskRoute_App appRoute -> appRouteSegment appRoute
744+ ObeliskRoute_Resource resourceRoute -> resourceRouteSegment resourceRoute
745+
746+-- | A function which gives a sane default for how to encode Obelisk resource routes. It's given in this form, because it will
747+-- be combined with other such segment encoders before 'pathComponentEncoder' turns it into a proper 'Encoder'.
748+resourceRouteSegment :: (MonadError Text check, MonadError Text parse) => ResourceRoute a -> SegmentResult check parse a
749+resourceRouteSegment = \case
750+ ResourceRoute_Static -> PathSegment "static" $ pathOnlyEncoder
751+ ResourceRoute_Ghcjs -> PathSegment "ghcjs" $ pathOnlyEncoder
752+ ResourceRoute_JSaddleWarp -> PathSegment "jsaddle" $ jsaddleWarpRouteEncoder
753+
754+data JSaddleWarpRoute :: * -> * where
755+ JSaddleWarpRoute_JavaScript :: JSaddleWarpRoute ()
756+ JSaddleWarpRoute_WebSocket :: JSaddleWarpRoute ()
757+ JSaddleWarpRoute_Sync :: JSaddleWarpRoute [Text]
758+
759+jsaddleWarpRouteEncoder :: (MonadError Text check, MonadError Text parse) => Encoder check parse (R JSaddleWarpRoute) PageName
760+jsaddleWarpRouteEncoder = pathComponentEncoder $ \case
761+ JSaddleWarpRoute_JavaScript -> PathSegment "jsaddle.js" $ unitEncoder mempty
762+ JSaddleWarpRoute_WebSocket -> PathEnd $ unitEncoder mempty
763+ JSaddleWarpRoute_Sync -> PathSegment "sync" $ pathOnlyEncoder
764+
765+instance GShow appRoute => GShow (ObeliskRoute appRoute) where
766+ gshowsPrec prec = \case
767+ ObeliskRoute_App appRoute -> showParen (prec > 10) $
768+ showString "ObeliskRoute_App " . gshowsPrec 11 appRoute
769+ ObeliskRoute_Resource appRoute -> showParen (prec > 10) $
770+ showString "ObeliskRoute_Resource " . gshowsPrec 11 appRoute
771+
772+data IndexOnlyRoute :: * -> * where
773+ IndexOnlyRoute :: IndexOnlyRoute ()
774+
775+indexOnlyRouteSegment :: (Applicative check, MonadError Text parse) => IndexOnlyRoute a -> SegmentResult check parse a
776+indexOnlyRouteSegment = \case
777+ IndexOnlyRoute -> PathEnd $ unitEncoder mempty
778+
779+indexOnlyRouteEncoder :: (MonadError Text check, MonadError Text parse) => Encoder check parse (R IndexOnlyRoute) PageName
780+indexOnlyRouteEncoder = pathComponentEncoder indexOnlyRouteSegment
781+
782+someSumEncoder :: (Applicative check, Applicative parse) => Encoder check parse (Some (Sum a b)) (Either (Some a) (Some b))
783+someSumEncoder = Encoder $ pure $ EncoderImpl
784+ { _encoderImpl_encode = \(Some.This t) -> case t of
785+ InL l -> Left $ Some.This l
786+ InR r -> Right $ Some.This r
787+ , _encoderImpl_decode = pure . \case
788+ Left (Some.This l) -> Some.This (InL l)
789+ Right (Some.This r) -> Some.This (InR r)
790+ }
791+
792+data Void1 :: * -> * where {}
793+
794+instance Universe (Some Void1) where
795+ universe = []
796+
797+void1Encoder :: (Applicative check, MonadError Text parse) => Encoder check parse (Some Void1) a
798+void1Encoder = Encoder $ pure $ EncoderImpl
799+ { _encoderImpl_encode = \case
800+ Some.This f -> case f of {}
801+ , _encoderImpl_decode = \_ -> throwError "void1Encoder: can't decode anything"
802+ }
803+
804+instance GShow Void1 where
805+ gshowsPrec _ = \case {}
806+
807+concat <$> mapM deriveRouteComponent
808+ [ ''ResourceRoute
809+ , ''JSaddleWarpRoute
810+ , ''IndexOnlyRoute
811+ ]
812+
813+makePrisms ''ObeliskRoute
814+
815+deriveGEq ''Void1
816+deriveGCompare ''Void1
817+
818+--TODO: decodeURIComponent as appropriate
lib/route/src/Obelisk/Route/Frontend.hsnew file+346
@@ -0,0 +1,346 @@
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+ , maybeRoute
37+ , maybeRoute_
38+ , maybeRouted
39+ , runRouteViewT
40+ , SetRouteT(..)
41+ , SetRoute(..)
42+ , runSetRouteT
43+ , mapSetRouteT
44+ ) where
45+
46+import Prelude hiding ((.), id)
47+
48+import Obelisk.Route
49+
50+import Control.Category (Category (..), (.))
51+import Control.Category.Cartesian
52+import Control.Lens hiding (Bifunctor, bimap, universe)
53+import Control.Monad.Fix
54+import Control.Monad.Primitive
55+import Control.Monad.Ref
56+import Control.Monad.Trans
57+import Control.Monad.Trans.Control
58+import Control.Monad.Trans.Reader
59+import Data.Coerce
60+import Data.Constraint (Dict (..))
61+import Data.Dependent.Sum (DSum (..))
62+import Data.GADT.Compare
63+import Data.Monoid
64+import qualified Data.Text as T
65+import Data.Functor.Compose
66+import Reflex.Class
67+import Reflex.Host.Class
68+import Reflex.PostBuild.Class
69+import Reflex.TriggerEvent.Class
70+import Reflex.PerformEvent.Class
71+import Reflex.EventWriter.Class
72+import Reflex.EventWriter.Base
73+import Reflex.Dynamic
74+import Reflex.Dom.Builder.Class
75+import Data.Type.Coercion
76+import Language.Javascript.JSaddle --TODO: Get rid of this - other platforms can also be routed
77+import Reflex.Dom.Core
78+import qualified GHCJS.DOM.Types as DOM
79+import Network.URI
80+#if defined(ios_HOST_OS)
81+import Data.Maybe (fromMaybe)
82+import qualified Data.List as L
83+#endif
84+
85+import Unsafe.Coerce
86+
87+infixr 5 :~
88+pattern (:~) :: Reflex t => f a -> Dynamic t a -> DSum f (Compose (Dynamic t) Identity)
89+pattern a :~ b <- a :=> (coerceDynamic . getCompose -> b)
90+
91+class Routed t r m | m -> t r where
92+ askRoute :: m (Dynamic t r)
93+
94+instance Monad m => Routed t r (RoutedT t r m) where
95+ askRoute = RoutedT ask
96+
97+newtype RoutedT t r m a = RoutedT { unRoutedT :: ReaderT (Dynamic t r) m a }
98+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, HasJSContext, MonadIO, MonadReflexCreateTrigger t, HasDocument)
99+
100+instance Prerender js m => Prerender js (RoutedT t r m) where
101+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
102+
103+instance Requester t m => Requester t (RoutedT t r m) where
104+ type Request (RoutedT t r m) = Request m
105+ type Response (RoutedT t r m) = Response m
106+ requesting = RoutedT . requesting
107+ requesting_ = RoutedT . requesting_
108+
109+#ifndef ghcjs_HOST_OS
110+deriving instance MonadJSM m => MonadJSM (RoutedT t r m)
111+#endif
112+
113+instance PerformEvent t m => PerformEvent t (RoutedT t r m) where
114+ type Performable (RoutedT t r m) = Performable m
115+ performEvent = lift . performEvent
116+ performEvent_ = lift . performEvent_
117+
118+instance MonadRef m => MonadRef (RoutedT t r m) where
119+ type Ref (RoutedT t r m) = Ref m
120+ newRef = lift . newRef
121+ readRef = lift . readRef
122+ writeRef r = lift . writeRef r
123+
124+instance HasJS x m => HasJS x (RoutedT t r m) where
125+ type JSX (RoutedT t r m) = JSX m
126+ liftJS = lift . liftJS
127+
128+deriving instance EventWriter t w m => EventWriter t w (RoutedT t r m)
129+
130+instance MonadTransControl (RoutedT t r) where
131+ type StT (RoutedT t r) a = StT (ReaderT (Dynamic t r)) a
132+ liftWith = defaultLiftWith RoutedT unRoutedT
133+ restoreT = defaultRestoreT RoutedT
134+
135+instance PrimMonad m => PrimMonad (RoutedT t r m ) where
136+ type PrimState (RoutedT t r m) = PrimState m
137+ primitive = lift . primitive
138+
139+instance DomBuilder t m => DomBuilder t (RoutedT t r m) where
140+ type DomBuilderSpace (RoutedT t r m) = DomBuilderSpace m
141+
142+instance Adjustable t m => Adjustable t (RoutedT t r m) where
143+ runWithReplace a0 a' = RoutedT $ runWithReplace (coerce a0) $ coerceEvent a'
144+ traverseIntMapWithKeyWithAdjust f a0 a' = RoutedT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
145+ traverseDMapWithKeyWithAdjust f a0 a' = RoutedT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
146+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = RoutedT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
147+
148+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (RoutedT t r m) where
149+ tellQueryIncremental = lift . tellQueryIncremental
150+ askQueryResult = lift askQueryResult
151+ queryIncremental = lift . queryIncremental
152+
153+runRoutedT :: RoutedT t r m a -> Dynamic t r -> m a
154+runRoutedT = runReaderT . unRoutedT
155+
156+mapRoutedT :: (m a -> n a) -> RoutedT t r m a -> RoutedT t r n a
157+mapRoutedT f = RoutedT . mapReaderT f . unRoutedT
158+
159+withRoutedT :: (Dynamic t r -> Dynamic t r') -> RoutedT t r' m a -> RoutedT t r m a
160+withRoutedT f = RoutedT . withReaderT f . unRoutedT
161+
162+subRoute_ :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => (forall a. r a -> RoutedT t a m ()) -> RoutedT t (R r) m ()
163+subRoute_ f = factorRouted $ strictDynWidget_ $ \(c :=> r') -> do
164+ runRoutedT (f c) r'
165+
166+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)
167+subRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
168+ runRoutedT (f c) r'
169+
170+maybeRoute_ :: (MonadFix m, MonadHold t m, Adjustable t m) => m () -> RoutedT t r m () -> RoutedT t (Maybe r) m ()
171+maybeRoute_ n j = maybeRouted $ strictDynWidget_ $ \case
172+ Nothing -> n
173+ Just r -> runRoutedT j r
174+
175+maybeRoute :: (MonadFix m, MonadHold t m, Adjustable t m) => m a -> RoutedT t r m a -> RoutedT t (Maybe r) m (Dynamic t a)
176+maybeRoute n j = maybeRouted $ strictDynWidget $ \case
177+ Nothing -> n
178+ Just r -> runRoutedT j r
179+
180+{-
181+maybeRoute :: (MonadFix m, MonadHold t m, GEq r, Adjustable t m) => m a -> RoutedT t r m a -> RoutedT t (Maybe r) m a
182+maybeRoute f = factorRouted $ strictDynWidget $ \(c :=> r') -> do
183+ runRoutedT (f c) r'
184+-}
185+
186+dsumValueCoercion :: Coercion f g -> Coercion (DSum k f) (DSum k g)
187+dsumValueCoercion Coercion = Coercion
188+
189+dynamicIdentityCoercion :: Coercion (Compose (Dynamic t) Identity) (Dynamic t)
190+dynamicIdentityCoercion = unsafeCoerce (Coercion :: Coercion (Identity ()) ()) --TODO: Is it possible to prove this?
191+
192+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
193+factorRouted r = RoutedT $ ReaderT $ \d -> do
194+ d' <- factorDyn d
195+ runRoutedT r $ (coerceWith (dynamicCoercion $ dsumValueCoercion dynamicIdentityCoercion) d')
196+
197+maybeRouted :: (Reflex t, MonadFix m, MonadHold t m) => RoutedT t (Maybe (Dynamic t a)) m b -> RoutedT t (Maybe a) m b
198+maybeRouted r = RoutedT $ ReaderT $ \d -> do
199+ d' <- maybeDyn d
200+ runRoutedT r d'
201+
202+-- | WARNING: The input 'Dynamic' must be fully constructed when this is run
203+strictDynWidget :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m b) -> RoutedT t a m (Dynamic t b)
204+strictDynWidget f = RoutedT $ ReaderT $ \r -> do
205+ r0 <- sample $ current r
206+ (result0, result') <- runWithReplace (f r0) $ f <$> updated r
207+ holdDyn result0 result'
208+
209+strictDynWidget_ :: (MonadSample t m, MonadHold t m, Adjustable t m) => (a -> m ()) -> RoutedT t a m ()
210+strictDynWidget_ f = RoutedT $ ReaderT $ \r -> do
211+ r0 <- sample $ current r
212+ (_, _) <- runWithReplace (f r0) $ f <$> updated r
213+ pure ()
214+
215+newtype SetRouteT t r m a = SetRouteT { unSetRouteT :: EventWriterT t (Endo r) m a }
216+ deriving (Functor, Applicative, Monad, MonadFix, MonadTrans, NotReady t, MonadHold t, MonadSample t, PostBuild t, TriggerEvent t, HasJSContext, MonadIO, MonadReflexCreateTrigger t, HasDocument, DomBuilder t)
217+
218+mapSetRouteT :: (forall x. m x -> n x) -> SetRouteT t r m a -> SetRouteT t r n a
219+mapSetRouteT f (SetRouteT x) = SetRouteT (mapEventWriterT f x)
220+
221+runSetRouteT :: (Reflex t, Monad m) => SetRouteT t r m a -> m (a, Event t (Endo r))
222+runSetRouteT = runEventWriterT . unSetRouteT
223+
224+class Reflex t => SetRoute t r m | m -> t r where
225+ setRoute :: Event t r -> m ()
226+ modifyRoute :: Event t (r -> r) -> m ()
227+ setRoute = modifyRoute . fmap const
228+
229+instance (Reflex t, Monad m) => SetRoute t r (SetRouteT t r m) where
230+ modifyRoute = SetRouteT . tellEvent . fmap Endo
231+
232+instance (Monad m, SetRoute t r m) => SetRoute t r (RoutedT t r' m) where
233+ modifyRoute = lift . modifyRoute
234+
235+instance Prerender js m => Prerender js (SetRouteT t r m) where
236+ prerenderClientDict = fmap (\Dict -> Dict) (prerenderClientDict :: Maybe (Dict (PrerenderClientConstraint js m)))
237+
238+instance Requester t m => Requester t (SetRouteT t r m) where
239+ type Request (SetRouteT t r m) = Request m
240+ type Response (SetRouteT t r m) = Response m
241+ requesting = SetRouteT . requesting
242+ requesting_ = SetRouteT . requesting_
243+
244+#ifndef ghcjs_HOST_OS
245+deriving instance MonadJSM m => MonadJSM (SetRouteT t r m)
246+#endif
247+
248+instance PerformEvent t m => PerformEvent t (SetRouteT t r m) where
249+ type Performable (SetRouteT t r m) = Performable m
250+ performEvent = lift . performEvent
251+ performEvent_ = lift . performEvent_
252+
253+instance MonadRef m => MonadRef (SetRouteT t r m) where
254+ type Ref (SetRouteT t r m) = Ref m
255+ newRef = lift . newRef
256+ readRef = lift . readRef
257+ writeRef r = lift . writeRef r
258+
259+instance HasJS x m => HasJS x (SetRouteT t r m) where
260+ type JSX (SetRouteT t r m) = JSX m
261+ liftJS = lift . liftJS
262+
263+instance PrimMonad m => PrimMonad (SetRouteT t r m ) where
264+ type PrimState (SetRouteT t r m) = PrimState m
265+ primitive = lift . primitive
266+
267+instance (MonadHold t m, Adjustable t m) => Adjustable t (SetRouteT t r m) where
268+ runWithReplace a0 a' = SetRouteT $ runWithReplace (coerce a0) $ coerceEvent a'
269+ traverseIntMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseIntMapWithKeyWithAdjust (coerce f) (coerce a0) $ coerce a'
270+ traverseDMapWithKeyWithAdjust f a0 a' = SetRouteT $ traverseDMapWithKeyWithAdjust (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
271+ traverseDMapWithKeyWithAdjustWithMove f a0 a' = SetRouteT $ traverseDMapWithKeyWithAdjustWithMove (\k v -> coerce $ f k v) (coerce a0) $ coerce a'
272+
273+instance (Monad m, MonadQuery t vs m) => MonadQuery t vs (SetRouteT t r m) where
274+ tellQueryIncremental = lift . tellQueryIncremental
275+ askQueryResult = lift askQueryResult
276+ queryIncremental = lift . queryIncremental
277+
278+runRouteViewT
279+ :: forall t m r a.
280+ ( TriggerEvent t m
281+ , PerformEvent t m
282+ , MonadHold t m
283+ , MonadJSM m
284+ , MonadJSM (Performable m)
285+ , MonadFix m
286+ )
287+ => (Encoder Identity Identity r PageName)
288+ -> RoutedT t r (SetRouteT t r m) a
289+ -> m a
290+runRouteViewT routeEncoder a = do
291+ rec historyState <- manageHistory $ HistoryCommand_PushState <$> setState
292+ let theEncoder = pageNameEncoder . hoistParse (pure . runIdentity) routeEncoder
293+ -- NB: The can only fail if the uriPath doesn't begin with a '/' or if the uriQuery
294+ -- is nonempty, but begins with a character that isn't '?'. Since we don't expect
295+ -- this ever to happen, we'll just handle it by failing completely with 'error'.
296+ route :: Dynamic t r
297+ route = fmap (errorLeft . tryDecode theEncoder . (adaptedUriPath &&& uriQuery) . _historyItem_uri) historyState
298+ where
299+ errorLeft (Left e) = error (T.unpack e)
300+ errorLeft (Right x) = x
301+ (result, changeState) <- runSetRouteT $ runRoutedT a route
302+ let f (currentHistoryState, oldRoute) change =
303+ let newRoute = appEndo change oldRoute
304+ (newPath, newQuery) = encode theEncoder newRoute
305+ in HistoryStateUpdate
306+ { _historyStateUpdate_state = DOM.SerializedScriptValue jsNull
307+ -- We always provide "" as the title. On Firefox, Chrome, and
308+ -- Edge, this parameter does nothing. On Safari, "" has the
309+ -- same behavior as other browsers (as far as I can tell), but
310+ -- anything else sets the title for the back button list item
311+ -- the *next* time pushState is called, unless the page title
312+ -- is changed in the interim. Since the Safari functionality
313+ -- is near-pointless and also confusing, I'm not going to even
314+ -- bother exposing it; if there ends up being a real use case,
315+ -- we can change this function later to accommodate.
316+ -- See: https://github.com/whatwg/html/issues/2174
317+ , _historyStateUpdate_title = ""
318+ , _historyStateUpdate_uri = Just $ setAdaptedUriPath newPath $ (_historyItem_uri currentHistoryState)
319+ { uriQuery = newQuery
320+ }
321+ }
322+ setState = attachWith f ((,) <$> current historyState <*> current route) changeState
323+ return result
324+
325+-- On ios due to sandboxing when loading the page from a file adapt the
326+-- path to be based on the hash.
327+
328+adaptedUriPath :: URI -> String
329+#if defined(ios_HOST_OS)
330+adaptedUriPath = hashToPath . uriFragment
331+
332+hashToPath :: String -> String
333+hashToPath = ('/' :) . fromMaybe "" . L.stripPrefix "#"
334+#else
335+adaptedUriPath = uriPath
336+#endif
337+
338+setAdaptedUriPath :: String -> URI -> URI
339+#if defined(ios_HOST_OS)
340+setAdaptedUriPath s u = u { uriFragment = pathToHash s }
341+
342+pathToHash :: String -> String
343+pathToHash = ('#' :) . fromMaybe "" . L.stripPrefix "/"
344+#else
345+setAdaptedUriPath s u = u { uriPath = s }
346+#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
lib/selftest/src/Obelisk/SelfTest.hs+7−1
@@ -26,7 +26,7 @@ import System.Exit (ExitCode (..))
2626import System.Info
2727import System.IO (Handle, hClose)
2828import System.IO.Temp
29−import System.Process (readProcessWithExitCode)
29+import System.Process (readProcessWithExitCode, CreateProcess(cwd), readCreateProcessWithExitCode, proc)
3030import System.Timeout
3131import Test.Hspec
3232import Test.HUnit.Base
@@ -119,6 +119,12 @@ main = do
119119 it "works with default impl" $ inTmp $ \_ -> run "ob" ["init"]
120120 it "works with master branch impl" $ inTmp $ \_ -> run "ob" ["init", "--branch", "master"]
121121 it "works with symlink" $ inTmp $ \_ -> run "ob" ["init", "--symlink", obeliskImpl]
122+ it "doesn't silently overwrite existing files" $ withSystemTempDirectory "ob-init" $ \dir -> do
123+ let p force = (proc "ob" $ "--no-handoff" : "init" : if force then ["--force"] else []) { cwd = Just dir }
124+ (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p False) ""
125+ (ExitFailure _, _, _) <- readCreateProcessWithExitCode (p False) ""
126+ (ExitSuccess, _, _) <- readCreateProcessWithExitCode (p True) ""
127+ pure ()
122128
123129 it "doesn't create anything when given an invalid impl" $ inTmp $ \tmp -> do
124130 void $ errExit False $ run "ob" ["init", "--symlink", "/dev/null"]
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+7−4
@@ -7,8 +7,10 @@ library
77 hs-source-dirs: src
88 build-depends: base
99 , common
10+ , obelisk-frontend
11+ , obelisk-route
1012 , reflex-dom
11− , static
13+ , obelisk-generated-static
1214 , text
1315 exposed-modules:
1416 Frontend
@@ -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
23− , static
24− , text
27+ , obelisk-generated-static
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+10−6
@@ -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
10−import Static
12+import Common.Route
13+import 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