227 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
			
		
		
	
	
			227 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Haskell
		
	
	
	
	
	
| {-# 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 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)
 | |
| 
 | |
| hledgerApiVersion="1.1.98"
 | |
| 
 | |
| -- https://github.com/docopt/docopt.hs#readme
 | |
| doc :: Docopt
 | |
| doc = [docopt|
 | |
| hledger-api 1.1.98
 | |
| 
 | |
| 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|--info
 | |
| 
 | |
| 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               show usage
 | |
|      --help        show manual
 | |
|      --man         show manual with man
 | |
|      --info        show manual with info
 | |
| |]
 | |
| 
 | |
| 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')) $ exitWithUsage doc
 | |
|   when (isPresent args (longOption "help")) $ printHelpForTopic "api" >> exitSuccess
 | |
|   when (isPresent args (longOption "man"))  $ runManForTopic "api" >> exitSuccess
 | |
|   when (isPresent args (longOption "info")) $ runInfoForTopic "api" >> exitSuccess
 | |
|   when (isPresent args (longOption "version")) $ putStrLn 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 Nothing Nothing True 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
 | |
|       )
 | |
|       --
 | |
|       :<|> serveDirectory staticdir
 | |
|       where
 | |
|         accountnamesH = return $ journalAccountNames j
 | |
|         transactionsH = return $ jtxns j
 | |
|         pricesH       = return $ jmarketprices 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
 | |
| 
 | |
| instance ToJSON ClearedStatus 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 Price 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)
 | |
|     ]
 | |
| instance ToSchema ClearedStatus
 | |
| 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 Price
 | |
| instance ToSchema MarketPrice
 | |
| instance ToSchema PostingType
 | |
| instance ToSchema Posting
 | |
| instance ToSchema Transaction
 | |
| instance ToSchema Account
 | |
| -- instance ToSchema AccountTransactionsReport
 |