{-| Copyright : (C) 2015-2016, University of Twente License : BSD2 (see the file LICENSE) Maintainer : Christiaan Baaij -} module Data.Aeson.Extra where import Control.Exception (throw) import qualified Data.Ix as Ix import qualified Data.Text as T import Data.Text (Text,pack,unpack) import Data.List (intercalate) import Data.Aeson (FromJSON, Result (..), fromJSON, json) import Data.Attoparsec.Lazy (Result (..), parse) import Data.ByteString.Lazy (ByteString) import qualified Data.ByteString.Lazy as BS import qualified Data.ByteString.Lazy.Char8 as BSChar import System.FilePath () import Clash.Util (ClashException(..)) import SrcLoc (mkGeneralSrcSpan) import FastString (mkFastString) import GHC.Stack (HasCallStack) -- Quick and dirty way of replacing fake escapes in naively converted bytestring replaceCommonEscapes :: Text -> Text replaceCommonEscapes = ( T.replace (pack "\\n") (pack "\n") ) . ( T.replace (pack "\\\\") (pack "\\") ) . ( T.replace (pack "\\\"") (pack "\"") ) genLineErr' :: [Text] -> (Int, Int) -> Int -> Text genLineErr' allLines range errorLineN = T.unlines [ T.concat [ if i == errorLineN then pack ">> " else pack " " , pack $ show i , pack ". " , allLines !! i ] | i <- Ix.range range] -- | Pretty print part of json file related to error genLineErr :: ByteString -> ByteString -> Text genLineErr full part = genLineErr' allLines interval errorLineN where -- Determine interval, and pass to helper function nLastLines = 1 + (length $ T.lines $ replaceCommonEscapes $ pack $ show part) errorLineN = length allLines - nLastLines + 1 allLines = T.lines $ replaceCommonEscapes $ pack $ show full interval = (max 0 (errorLineN - 5), min (max 0 $ length allLines - 1) (errorLineN + 5)) -- | Parse a ByteString according to the given JSON template. Throws exception -- if it fails. decodeOrErr :: (HasCallStack, FromJSON a) => FilePath -- ^ Path read from (for error message) -> ByteString -- ^ Bytestring to parse -> a decodeOrErr path contents = case parse json contents of Done leftover v -> case fromJSON v of Success _ | BS.any notWhitespace leftover -> clashError ("After parsing " ++ show path ++ ", found unparsed trailing garbage:\n" ++ BSChar.unpack leftover) Success a -> a Error msg -> clashError ( "Could not deduce valid scheme for json in " ++ show path ++ ". Error was: \n\n" ++ msg ) -- JSON parse error: Fail bytes cntxs msg -> clashError ( "Could not read or parse json in " ++ show path ++ ". " ++ (if null cntxs then "" else "Context was:\n " ++ intercalate "\n " cntxs) ++ "\n\nError reported by Attoparsec was:\n " ++ msg ++ "\n\nApproximate location of error:\n\n" -- HACK: Replace with proper parser/fail logic in future. Or don't. It's not important. ++ (unpack $ genLineErr contents bytes) ) where loc = mkGeneralSrcSpan $ mkFastString path clashError msg = throw $ ClashException loc msg Nothing notWhitespace c = BS.notElem c whitespace where whitespace = BSChar.pack " \t\n\r"