mirror of
https://github.com/simonmichael/hledger.git
synced 2024-12-27 12:24:43 +03:00
2d1b53c597
[ci skip]
276 lines
9.7 KiB
Haskell
276 lines
9.7 KiB
Haskell
{-# LANGUAGE CPP #-}
|
|
{-# LANGUAGE DataKinds #-}
|
|
{-# LANGUAGE DeriveGeneric #-}
|
|
{-# LANGUAGE OverloadedStrings #-}
|
|
{-# LANGUAGE PolyKinds #-}
|
|
{-# LANGUAGE QuasiQuotes #-}
|
|
{-# LANGUAGE TypeFamilies #-}
|
|
{-# LANGUAGE TypeOperators #-}
|
|
{-# LANGUAGE FlexibleInstances #-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
{-# LANGUAGE ScopedTypeVariables #-}
|
|
|
|
module Main where
|
|
|
|
import Lens.Micro ((&), (.~))
|
|
import Control.Monad
|
|
import Data.Aeson
|
|
import qualified Data.ByteString.Lazy.Char8 as BL8
|
|
import Data.Decimal
|
|
import Data.Default
|
|
import qualified Data.Map as M
|
|
import Data.Proxy
|
|
import Data.String (fromString)
|
|
import Data.Swagger
|
|
import Data.Text hiding (map,reverse)
|
|
import Network.Wai as Wai
|
|
import Network.Wai.Handler.Warp as Warp
|
|
import Network.Wai.Middleware.RequestLogger
|
|
import Safe
|
|
import Servant
|
|
import Servant.Swagger
|
|
import System.Console.Docopt
|
|
import System.Environment (getArgs)
|
|
import System.Exit
|
|
import Text.Printf
|
|
|
|
import Hledger.Query
|
|
import Hledger.Cli hiding (Reader, version)
|
|
|
|
#ifdef VERSION
|
|
hledgerApiVersion = VERSION
|
|
#else
|
|
hledgerApiVersion = "dev build"
|
|
#endif
|
|
|
|
-- https://github.com/docopt/docopt.hs#readme
|
|
-- XXX VERSION is "X.Y" and the quotes appear in the output
|
|
doc :: Docopt
|
|
doc = [docopt|
|
|
hledger-api VERSION
|
|
|
|
Serves hledger data and reports as a JSON web API.
|
|
|
|
Usage:
|
|
hledger-api [options]
|
|
start API server
|
|
hledger-api --swagger
|
|
print API docs in Swagger 2.0 format
|
|
hledger-api --version
|
|
hledger-api -h|--help
|
|
|
|
Options:
|
|
-f --file FILE use a different input file
|
|
(default: $LEDGER_FILE or ~/.hledger.journal)
|
|
-d --static-dir DIR serve files from a different directory
|
|
(default: .)
|
|
--host IPADDR listen on this IP address (default: 127.0.0.1)
|
|
-p --port PORT listen on this TCP port (default: 8001)
|
|
--version show version
|
|
-h --help show usage
|
|
|]
|
|
|
|
swaggerSpec :: Swagger
|
|
swaggerSpec = toSwagger (Proxy :: Proxy HledgerApi)
|
|
& info.title .~ "hledger API"
|
|
& info.version .~ pack hledgerApiVersion
|
|
& info.description .~ Just "This is the API provided by hledger-api for reading hledger data"
|
|
& info.license .~ Just (License "GPLv3+" (Nothing))
|
|
|
|
main :: IO ()
|
|
main = do
|
|
args <- getArgs >>= parseArgsOrExit doc
|
|
when (isPresent args (shortOption 'h') || isPresent args (longOption "help")) $ exitWithUsage doc
|
|
when (isPresent args (longOption "version")) $ putStrLn ("hledger-api " ++ hledgerApiVersion) >> exitSuccess
|
|
when (isPresent args (longOption "swagger")) $ BL8.putStrLn (encode swaggerSpec) >> exitSuccess
|
|
let
|
|
defh = "127.0.0.1"
|
|
h = getArgWithDefault args defh (longOption "host")
|
|
defp = "8001"
|
|
p <- case readMay $ getArgWithDefault args defp (longOption "port") of
|
|
Nothing -> exitWithUsage doc
|
|
Just n -> return n
|
|
deff <- defaultJournalPath
|
|
let f = getArgWithDefault args deff (longOption "file")
|
|
requireJournalFileExists f
|
|
let
|
|
defd = "."
|
|
d = getArgWithDefault args defd (longOption "static-dir")
|
|
readJournalFile def f >>= either error' (serveApi h p d f)
|
|
|
|
serveApi :: String -> Int -> FilePath -> FilePath -> Journal -> IO ()
|
|
serveApi h p d f j = do
|
|
printf "Starting web api http://%s:%d/api/v1 for %s\n" h p f
|
|
printf "and file server http://%s:%d for %s/\n" h p d
|
|
printf "Press ctrl-c to quit\n"
|
|
let warpsettings = defaultSettings
|
|
& setHost (fromString h)
|
|
& setPort p
|
|
Warp.runSettings warpsettings $
|
|
logStdout $
|
|
hledgerApiApp d j
|
|
|
|
type HledgerApi =
|
|
"api" :> "v1" :>
|
|
(
|
|
"accountnames" :> Get '[JSON] [AccountName]
|
|
:<|> "transactions" :> Get '[JSON] [Transaction]
|
|
:<|> "prices" :> Get '[JSON] [MarketPrice]
|
|
:<|> "commodities" :> Get '[JSON] [CommoditySymbol]
|
|
:<|> "accounts" :> Get '[JSON] [Account]
|
|
:<|> "accounts" :> Capture "acct" AccountName :> Get '[JSON] AccountTransactionsReport
|
|
)
|
|
|
|
type HledgerSwaggerApi =
|
|
"swagger.json" :> Get '[JSON] Swagger
|
|
:<|> HledgerApi
|
|
|
|
type HledgerSwaggerFilesApi =
|
|
HledgerSwaggerApi
|
|
:<|> Raw
|
|
|
|
hledgerApiApp :: FilePath -> Journal -> Wai.Application
|
|
hledgerApiApp staticdir j = Servant.serve api server
|
|
where
|
|
api :: Proxy HledgerSwaggerFilesApi
|
|
api = Proxy
|
|
|
|
server :: Server HledgerSwaggerFilesApi
|
|
server =
|
|
(
|
|
return swaggerSpec
|
|
--
|
|
:<|> accountnamesH
|
|
:<|> transactionsH
|
|
:<|> pricesH
|
|
:<|> commoditiesH
|
|
:<|> accountsH
|
|
:<|> accounttransactionsH
|
|
)
|
|
--
|
|
:<|> serveDirectoryFileServer staticdir
|
|
where
|
|
accountnamesH = return $ journalAccountNames j
|
|
transactionsH = return $ jtxns j
|
|
pricesH = return $ map priceDirectiveToMarketPrice $ jpricedirectives j
|
|
commoditiesH = return $ (M.keys . jinferredcommodities) j
|
|
accountsH = return $ ledgerTopAccounts $ ledgerFromJournal Hledger.Query.Any j
|
|
accounttransactionsH (a::AccountName) = do
|
|
-- d <- liftIO getCurrentDay
|
|
let
|
|
ropts = defreportopts
|
|
-- ropts' = ropts {depth_=Nothing
|
|
-- ,balancetype_=HistoricalBalance
|
|
-- }
|
|
q = Hledger.Query.Any --filterQuery (not . queryIsDepth) $ queryFromOpts d ropts'
|
|
thisacctq = Acct $ accountNameToAccountRegex a -- includes subs
|
|
return $ accountTransactionsReport ropts j q thisacctq
|
|
|
|
-- avoiding https://github.com/bos/aeson/issues/290 - no longer needed ?
|
|
--instance ToJSON Status where toJSON = genericToJSON defaultOptions -- avoiding https://github.com/bos/aeson/issues/290
|
|
--instance ToJSON GenericSourcePos where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON Decimal where toJSON = toJSON . show
|
|
--instance ToJSON Amount where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON AmountStyle where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON Side where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON DigitGroupStyle where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON MixedAmount where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON BalanceAssertion where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON AmountPrice where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON MarketPrice where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON PostingType where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON Posting where
|
|
-- toJSON Posting{..} =
|
|
-- object
|
|
-- ["pdate" .= toJSON pdate
|
|
-- ,"pdate2" .= toJSON pdate2
|
|
-- ,"pstatus" .= toJSON pstatus
|
|
-- ,"paccount" .= toJSON paccount
|
|
-- ,"pamount" .= toJSON pamount
|
|
-- ,"pcomment" .= toJSON pcomment
|
|
-- ,"ptype" .= toJSON ptype
|
|
-- ,"ptags" .= toJSON ptags
|
|
-- ,"pbalanceassertion" .= toJSON pbalanceassertion
|
|
-- ,"ptransactionidx" .= toJSON (maybe "" (show.tindex) ptransaction)
|
|
-- ]
|
|
--instance ToJSON Transaction where toJSON = genericToJSON defaultOptions
|
|
--instance ToJSON Account where
|
|
-- toJSON a =
|
|
-- object
|
|
-- ["aname" .= toJSON (aname a)
|
|
-- ,"aebalance" .= toJSON (aebalance a)
|
|
-- ,"aibalance" .= toJSON (aibalance a)
|
|
-- ,"anumpostings" .= toJSON (anumpostings a)
|
|
-- ,"aboring" .= toJSON (aboring a)
|
|
-- ,"aparentname" .= toJSON (maybe "" aname $ aparent a)
|
|
-- ,"asubs" .= toJSON (map toJSON $ asubs a)
|
|
-- ]
|
|
|
|
-- Convert things to JSON for serving to clients
|
|
instance ToJSON Status
|
|
instance ToJSON GenericSourcePos
|
|
instance ToJSON Decimal where toJSON = toJSON . show
|
|
instance ToJSON Amount
|
|
instance ToJSON AmountStyle
|
|
instance ToJSON Side
|
|
instance ToJSON DigitGroupStyle
|
|
instance ToJSON MixedAmount
|
|
instance ToJSON BalanceAssertion
|
|
instance ToJSON AmountPrice
|
|
instance ToJSON MarketPrice
|
|
instance ToJSON PostingType
|
|
instance ToJSON Posting where
|
|
toJSON Posting{..} =
|
|
object
|
|
["pdate" .= toJSON pdate
|
|
,"pdate2" .= toJSON pdate2
|
|
,"pstatus" .= toJSON pstatus
|
|
,"paccount" .= toJSON paccount
|
|
,"pamount" .= toJSON pamount
|
|
,"pcomment" .= toJSON pcomment
|
|
,"ptype" .= toJSON ptype
|
|
,"ptags" .= toJSON ptags
|
|
,"pbalanceassertion" .= toJSON pbalanceassertion
|
|
,"ptransactionidx" .= toJSON (maybe "" (show.tindex) ptransaction)
|
|
]
|
|
instance ToJSON Transaction
|
|
instance ToJSON Account where
|
|
toJSON a =
|
|
object
|
|
["aname" .= toJSON (aname a)
|
|
,"aebalance" .= toJSON (aebalance a)
|
|
,"aibalance" .= toJSON (aibalance a)
|
|
,"anumpostings" .= toJSON (anumpostings a)
|
|
,"aboring" .= toJSON (aboring a)
|
|
,"aparentname" .= toJSON (maybe "" aname $ aparent a)
|
|
,"asubs" .= toJSON (map toJSON $ asubs a)
|
|
]
|
|
|
|
-- convert things to Schema for swagger API description
|
|
instance ToSchema Status
|
|
instance ToSchema GenericSourcePos
|
|
instance ToSchema Decimal
|
|
where
|
|
declareNamedSchema _proxy = pure $ NamedSchema (Just "Decimal") schema
|
|
where
|
|
schema = mempty
|
|
& type_ .~ SwaggerNumber
|
|
& example .~ Just (toJSON (100 :: Decimal))
|
|
instance ToSchema Amount
|
|
instance ToSchema AmountStyle
|
|
instance ToSchema Side
|
|
instance ToSchema DigitGroupStyle
|
|
instance ToSchema MixedAmount
|
|
instance ToSchema BalanceAssertion
|
|
instance ToSchema AmountPrice
|
|
#if MIN_VERSION_swagger2(2,1,5)
|
|
where declareNamedSchema = genericDeclareNamedSchemaUnrestricted defaultSchemaOptions
|
|
#endif
|
|
instance ToSchema MarketPrice
|
|
instance ToSchema PostingType
|
|
instance ToSchema Posting
|
|
instance ToSchema Transaction
|
|
instance ToSchema Account
|
|
instance ToSchema AccountDeclarationInfo
|
|
-- instance ToSchema AccountTransactionsReport
|