Skip to content

Commit d8bebb6

Browse files
committed
imp: Improve the experience of errors when starting the web UI
Related issue: #885
1 parent 4b1919d commit d8bebb6

File tree

7 files changed

+105
-45
lines changed

7 files changed

+105
-45
lines changed

hledger-lib/Hledger/Read.hs

+32-18
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Hledger.Read (
2525
readJournalFiles,
2626
readJournalFile,
2727
requireJournalFileExists,
28+
requireJournalFileExists',
2829
ensureJournalFileExists,
2930

3031
-- * Journal parsing
@@ -172,32 +173,45 @@ readJournalFile iopts prefixedfile = do
172173
let
173174
(mfmt, f) = splitReaderPrefix prefixedfile
174175
iopts' = iopts{mformat_=asum [mfmt, mformat_ iopts]}
175-
requireJournalFileExists f
176-
t <- readFileOrStdinPortably f
177-
-- <- T.readFile f -- or without line ending translation, for testing
178-
ej <- readJournal iopts' (Just f) t
179-
case ej of
180-
Left e -> return $ Left e
181-
Right j | new_ iopts -> do
182-
ds <- previousLatestDates f
183-
let (newj, newds) = journalFilterSinceLatestDates ds j
184-
when (new_save_ iopts && not (null newds)) $ saveLatestDates newds f
185-
return $ Right newj
186-
Right j -> return $ Right j
176+
exists <- requireJournalFileExists' f
177+
case exists of
178+
Left e -> return $ Left e
179+
Right _ -> do
180+
t <- readFileOrStdinPortably f
181+
-- <- T.readFile f -- or without line ending translation, for testing
182+
ej <- readJournal iopts' (Just f) t
183+
case ej of
184+
Left e -> return $ Left e
185+
Right j | new_ iopts -> do
186+
ds <- previousLatestDates f
187+
let (newj, newds) = journalFilterSinceLatestDates ds j
188+
when (new_save_ iopts && not (null newds)) $ saveLatestDates newds f
189+
return $ Right newj
190+
Right j -> return $ Right j
187191

188192
--- ** utilities
189193

190194
-- | If the specified journal file does not exist (and is not "-"),
191195
-- give a helpful error and quit.
192196
requireJournalFileExists :: FilePath -> IO ()
193-
requireJournalFileExists "-" = return ()
194197
requireJournalFileExists f = do
198+
res <- requireJournalFileExists' f
199+
either (\e -> hPutStr stderr e >> exitFailure) pure res
200+
201+
-- | If the specified journal file does not exist (and is not "-"),
202+
-- give a helpful error.
203+
requireJournalFileExists' :: FilePath -> IO (Either String ())
204+
requireJournalFileExists' "-" = return $ Right ()
205+
requireJournalFileExists' f = do
195206
exists <- doesFileExist f
196-
unless exists $ do -- XXX might not be a journal file
197-
hPutStr stderr $ "The hledger journal file \"" <> f <> "\" was not found.\n"
198-
hPutStr stderr "Please create it first, eg with \"hledger add\" or a text editor.\n"
199-
hPutStr stderr "Or, specify an existing journal file with -f or LEDGER_FILE.\n"
200-
exitFailure
207+
if exists then
208+
return $ Right ()
209+
else
210+
return $ Left $ unlines [ "The hledger journal file \"" <> f <> "\" was not found."
211+
, "Please create it first, eg with \"hledger add\" or a text editor."
212+
, "Or, specify an existing journal file with -f or LEDGER_FILE."
213+
]
214+
201215

202216
-- | Ensure there is a journal file at the given path, creating an empty one if needed.
203217
-- On Windows, also ensure that the path contains no trailing dots

hledger-web/Hledger/Web/Application.hs

+14-15
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,18 @@
55

66
module Hledger.Web.Application
77
( makeApplication
8-
, makeFoundation
98
, makeFoundationWith
109
) where
1110

12-
import Data.IORef (newIORef, writeIORef)
11+
import Data.IORef (newIORef)
1312
import Network.Wai.Middleware.RequestLogger (logStdoutDev, logStdout)
1413
import Network.HTTP.Client (defaultManagerSettings)
1514
import Network.HTTP.Conduit (newManager)
15+
import System.IO (stderr, hPutStrLn)
1616
import Yesod.Default.Config
1717

18-
import Hledger.Data (Journal, nulljournal)
18+
import Hledger.Cli (withJournalTry)
19+
import Hledger.Data (Journal)
1920

2021
import Hledger.Web.Handler.AddR
2122
import Hledger.Web.Handler.MiscR
@@ -24,7 +25,8 @@ import Hledger.Web.Handler.UploadR
2425
import Hledger.Web.Handler.JournalR
2526
import Hledger.Web.Handler.RegisterR
2627
import Hledger.Web.Import
27-
import Hledger.Web.WebOptions (WebOpts(serve_,serve_api_), corsPolicy)
28+
import Hledger.Web.Error as WebError
29+
import Hledger.Web.WebOptions (WebOpts(serve_,serve_api_, cliopts_), corsPolicy)
2830

2931
-- This line actually creates our YesodDispatch instance. It is the second half
3032
-- of the call to mkYesodData which occurs in Foundation.hs. Please see the
@@ -35,22 +37,19 @@ mkYesodDispatch "App" resourcesApp
3537
-- performs initialization and creates a WAI application. This is also the
3638
-- place to put your migrate statements to have automatic database
3739
-- migrations handled by Yesod.
38-
makeApplication :: WebOpts -> Journal -> AppConfig DefaultEnv Extra -> IO Application
39-
makeApplication opts' j' conf' = do
40-
foundation <- makeFoundation conf' opts'
41-
writeIORef (appJournal foundation) j'
42-
(logWare . (corsPolicy opts')) <$> toWaiApp foundation
40+
makeApplication :: WebOpts -> AppConfig DefaultEnv Extra -> IO Application
41+
makeApplication opts' conf' = do
42+
let application = withJournalTry (toWaiApp <=< makeError) (cliopts_ opts') (toWaiApp <=< (\j -> makeFoundationWith j conf' opts'))
43+
(logWare . (corsPolicy opts')) <$> application
4344
where
4445
logWare | development = logStdoutDev
4546
| serve_ opts' || serve_api_ opts' = logStdout
4647
| otherwise = id
4748

48-
makeFoundation :: AppConfig DefaultEnv Extra -> WebOpts -> IO App
49-
makeFoundation conf opts' = do
50-
manager <- newManager defaultManagerSettings
51-
s <- staticSite
52-
jref <- newIORef nulljournal
53-
return $ App conf s manager opts' jref
49+
makeError :: String -> IO WebError.Error
50+
makeError err = do
51+
hPutStrLn stderr err
52+
pure $ WebError.Error err
5453

5554
-- Make a Foundation with the given Journal as its state.
5655
makeFoundationWith :: Journal -> AppConfig DefaultEnv Extra -> WebOpts -> IO App

hledger-web/Hledger/Web/Error.hs

+32
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{-# OPTIONS_GHC -fno-warn-orphans #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE QuasiQuotes #-}
4+
{-# LANGUAGE TemplateHaskell #-}
5+
{-# LANGUAGE TypeFamilies #-}
6+
7+
-- | Define the web application's if something went wrong, in the usual Yesod style.
8+
9+
module Hledger.Web.Error where
10+
11+
import Yesod
12+
13+
import Hledger.Web.Settings (widgetFile)
14+
15+
newtype Error = Error { problem :: String }
16+
17+
-- This is where we define the one route of the application if
18+
-- something went wrong. For a full explanation of the syntax,
19+
-- please see: http://www.yesodweb.com/book/handler
20+
mkYesod "Error" [parseRoutes|
21+
/ ErrorR GET
22+
|]
23+
24+
instance Yesod Error
25+
26+
-- | The error view.
27+
getErrorR :: Handler Html
28+
getErrorR = defaultLayout $ do
29+
Error problem <- getYesod
30+
setTitle "Error - hledger-web"
31+
$(widgetFile "error")
32+

hledger-web/Hledger/Web/Main.hs

+7-9
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ import Hledger.Web.WebOptions
3939
-- Run in fast reloading mode for yesod devel.
4040
hledgerWebDev :: IO (Int, Application)
4141
hledgerWebDev =
42-
withJournalDo (cliopts_ defwebopts) (defaultDevelApp loader . makeApplication defwebopts)
42+
defaultDevelApp loader (makeApplication defwebopts)
4343
where
4444
loader =
4545
Yesod.Default.Config.loadConfig
@@ -48,7 +48,7 @@ hledgerWebDev =
4848
-- Run normally.
4949
hledgerWebMain :: IO ()
5050
hledgerWebMain = do
51-
wopts@WebOpts{cliopts_=copts@CliOpts{debug_, rawopts_}} <- getHledgerWebOpts
51+
wopts@WebOpts{cliopts_=_copts@CliOpts{debug_, rawopts_}} <- getHledgerWebOpts
5252
when (debug_ > 0) $ printf "%s\n" prognameandversion >> printf "opts: %s\n" (show wopts)
5353
if
5454
| "help" `inRawOpts` rawopts_ -> putStr (showModeUsage webmode) >> exitSuccess
@@ -59,14 +59,12 @@ hledgerWebMain = do
5959
| "test" `inRawOpts` rawopts_ -> do
6060
-- remove --test and --, leaving other args for hspec
6161
(`withArgs` hledgerWebTest) . filter (`notElem` ["--test","--"]) =<< getArgs
62-
| otherwise -> withJournalDo copts (web wopts)
62+
| otherwise -> web wopts
6363

6464
-- | The hledger web command.
65-
web :: WebOpts -> Journal -> IO ()
66-
web opts j = do
67-
let initq = _rsQuery . reportspec_ $ cliopts_ opts
68-
j' = filterJournalTransactions initq j
69-
h = host_ opts
65+
web :: WebOpts -> IO ()
66+
web opts = do
67+
let h = host_ opts
7068
p = port_ opts
7169
u = base_url_ opts
7270
staticRoot = T.pack <$> file_url_ opts
@@ -76,7 +74,7 @@ web opts j = do
7674
,appRoot = T.pack u
7775
,appExtra = Extra "" Nothing staticRoot
7876
}
79-
app <- makeApplication opts j' appconfig
77+
app <- makeApplication opts appconfig
8078
-- XXX would like to allow a host name not just an IP address here
8179
_ <- printf "Serving web %s on %s:%d with base url %s\n"
8280
(if serve_api_ opts then "API" else "UI and API" :: String) h p u

hledger-web/hledger-web.cabal

+2
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ extra-source-files:
100100
templates/default-layout-wrapper.hamlet
101101
templates/default-layout.hamlet
102102
templates/edit-form.hamlet
103+
templates/error.hamlet
103104
templates/journal.hamlet
104105
templates/manage.hamlet
105106
templates/register.hamlet
@@ -131,6 +132,7 @@ library
131132
exposed-modules:
132133
Hledger.Web
133134
Hledger.Web.Application
135+
Hledger.Web.Error
134136
Hledger.Web.Foundation
135137
Hledger.Web.Handler.AddR
136138
Hledger.Web.Handler.EditR

hledger-web/templates/error.hamlet

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
<h2>
2+
Woops!
3+
4+
<p>
5+
<pre>
6+
#{problem}

hledger/Hledger/Cli/Utils.hs

+12-3
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ module Hledger.Cli.Utils
1212
(
1313
unsupportedOutputFormatError,
1414
withJournalDo,
15+
withJournalTry,
1516
writeOutput,
1617
writeOutputLazyText,
1718
journalTransform,
@@ -64,14 +65,22 @@ unsupportedOutputFormatError fmt = "Sorry, output format \""++fmt++"\" is unreco
6465
-- transformations according to options, and run a hledger command with it.
6566
-- Or, throw an error.
6667
withJournalDo :: CliOpts -> (Journal -> IO a) -> IO a
67-
withJournalDo opts cmd = do
68+
withJournalDo = withJournalTry error'
69+
70+
-- | Parse the user's specified journal file(s) as a Journal, maybe apply some
71+
-- transformations according to options, and run a hledger command with it.
72+
-- Or, do the default action.
73+
withJournalTry :: (String -> IO a) -> CliOpts -> (Journal -> IO a) -> IO a
74+
withJournalTry catch opts cmd = do
6875
-- We kludgily read the file before parsing to grab the full text, unless
6976
-- it's stdin, or it doesn't exist and we are adding. We read it strictly
7077
-- to let the add command work.
7178
journalpaths <- journalFilePathFromOpts opts
7279
files <- readJournalFiles (inputopts_ opts) journalpaths
73-
let transformed = journalTransform opts <$> files
74-
either error' cmd transformed -- PARTIAL:
80+
case files of
81+
Left e -> catch e
82+
Right journal -> cmd $ journalTransform opts journal -- PARTIAL:
83+
7584

7685
-- | Apply some extra post-parse transformations to the journal, if
7786
-- specified by options. These happen after journal validation, but

0 commit comments

Comments
 (0)