-- -- Copyright 2018, akashche at redhat.com -- -- Licensed under the Apache License, Version 2.0 (the "License"); -- you may not use this file except in compliance with the License. -- You may obtain a copy of the License at -- -- http://www.apache.org/licenses/LICENSE-2.0 -- -- Unless required by applicable law or agreed to in writing, software -- distributed under the License is distributed on an "AS IS" BASIS, -- WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -- See the License for the specific language governing permissions and -- limitations under the License. -- -- | -- JSON utilities -- -- Example of the data type, that supports JSON encoding and decoding with [Aeson](https://hackage.haskell.org/package/aeson): -- -- > data Foo = Foo -- > { foo :: Int -- > , bar :: Text -- > } deriving Generic -- > instance FromJSON Foo -- > instance ToJSON Foo -- {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE Strict #-} module VtUtils.JSON ( JSONDecodeFileException(..) , jsonDecodeFile , JSONDecodeError(..) , jsonDecodeText , JSONDecodeTextIOException(..) , jsonEncodeText , JSONGetError , jsonGet , jsonUnwrapUnaryOptions ) where import Prelude (Bool(..), Either(..), IO, Show, String, (.), ($), return, show) import Control.Exception (Exception, throwIO) import Data.Aeson (FromJSON, Value(..), ToJSON, defaultOptions, eitherDecode', eitherDecodeStrict') import Data.Aeson.Encode.Pretty (encodePrettyToTextBuilder) import Data.Aeson.Types (Options(..), (.:), parseEither) import Data.Monoid ((<>)) import Data.Text (Text, pack) import qualified Data.Text as Text import Data.Text.Encoding (encodeUtf8) import Data.Text.Lazy (toStrict) import Data.Text.Lazy.Builder (toLazyText) import VtUtils.Error (errorShow) import VtUtils.IO (ioWithFileBytes) import VtUtils.Text (textShow) -- | Encodes a data into a JSON @Text@ string -- -- Data must be an instance of [ToJSON](https://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#t:ToJSON) -- -- Arguments: -- -- * @data :: ToJSON@: some data that supports JSON serialization with Aeson -- -- Return value: JSON @Text@ string -- jsonEncodeText :: ToJSON a => a -> Text jsonEncodeText = toStrict . toLazyText . encodePrettyToTextBuilder -- | Error for `jsonDecodeText` function -- data JSONDecodeError = JSONDecodeError { jsonText :: Text -- ^ Specified text , message :: Text -- ^ Decoding error } instance Show JSONDecodeError where show e@(JSONDecodeError {jsonText, message}) = errorShow e $ "Error decoding JSON," <> " text: [" <> (Text.take 1024 jsonText) <> "]," <> " error: [" <> message <> "]" -- | Parses a JSON @Text@ string into a typed data -- -- Data type should be specified with a type annotation: -- -- Example: -- -- > -- > let Right (dt :: Foo) = jsonDecodeText text -- > -- -- Data must be an instance of [FromJSON](https://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#t:FromJSON) -- -- Returns an error if data cannot be decoded -- -- Arguments: -- -- * @text :: Text@: JSON @Text@ string to parse -- -- Return value: Decoded data or decoding error -- jsonDecodeText :: forall a . FromJSON a => Text -> Either JSONDecodeError a jsonDecodeText text = case eitherDecodeStrict' (encodeUtf8 text) :: Either String a of Left err -> Left $ JSONDecodeError text (pack err) Right res -> Right res -- | Exception for `jsonDecodeTextIO` function -- data JSONDecodeTextIOException = JSONDecodeTextIOException { err :: JSONDecodeError -- ^ Decoding error } instance Exception JSONDecodeTextIOException instance Show JSONDecodeTextIOException where show e@(JSONDecodeTextIOException {err}) = errorShow e $ textShow err -- | Exception for `jsonDecodeFile` function -- data JSONDecodeFileException = JSONDecodeFileException { filePath :: Text -- ^ Specified file path , message :: Text -- ^ Decoding error } instance Exception JSONDecodeFileException instance Show JSONDecodeFileException where show e@(JSONDecodeFileException {filePath, message}) = errorShow e $ "Error decoding JSON," <> " path: [" <> filePath <> "]," <> " error: [" <> message <> "]" -- | Parses contents of a specified JSON file into a typed data -- -- Data type should be specified with a type annotation: -- -- Example: -- -- > -- > dt <- jsonDecodeFile "path/to/foo.json" :: IO Foo -- > -- -- Data must be an instance of [FromJSON](https://hackage.haskell.org/package/aeson-1.4.2.0/docs/Data-Aeson.html#t:FromJSON) -- -- File contents are decoded as @UTF-8@ -- -- Throws an exception if file cannot be read or data cannot be decoded -- -- Arguments: -- -- * @path :: Text@: Path to JSON file -- -- Return value: Decoded data -- jsonDecodeFile :: forall a . FromJSON a => Text -> IO a jsonDecodeFile path = ioWithFileBytes path $ \bs -> case eitherDecode' bs :: Either String a of Left err -> throwIO $ JSONDecodeFileException path (pack err) Right res -> return res -- | Error for `jsonGet` function -- data JSONGetError = JSONGetError { objectField :: Text -- ^ Specified property , jsonValue :: Value -- ^ JSON value , message :: Text -- ^ Error message } instance Show JSONGetError where show e@(JSONGetError {objectField, jsonValue, message}) = errorShow e $ "Error accessing JSON property," <> " name: [" <> objectField <> "]," <> " text: [" <> (Text.take 1024 $ jsonEncodeText jsonValue) <> "]," <> " error: [" <> message <> "]" -- | Extract the field value from the specified JSON object -- -- Returns an error, if specified JSON @Value@ is not a JSON object, -- if it does't contain a specified field, if field type is different -- from the one specified in type annotation -- -- Data type should be specified with a type annotation: -- -- > -- > let obj = object -- > [ "foo" .= (42 :: Int) -- > , "bar" .= ("baz" :: Text) -- > ] -- > let Right (fooval :: Int) = jsonGet obj "foo" -- > let Right (barval :: Text) = jsonGet obj "bar" -- > -- -- Arguments: -- -- * @val :: Aeson.Value@: JSON value, must be a JSON object -- * @field :: Text@: Field name -- -- Return value: Field value or an error -- jsonGet :: forall a . FromJSON a => Value -> Text -> Either JSONGetError a jsonGet val field = case val of Object obj -> case parseEither (.: field) obj :: Either String a of Left err -> Left $ JSONGetError field val (pack err) Right a -> Right a _ -> Left $ JSONGetError field val "Invalid non-object JSON value specified" -- | JSON options with @unwrapUnaryRecords@ flag flipped to @True@ -- jsonUnwrapUnaryOptions :: Options jsonUnwrapUnaryOptions = defaultOptions { unwrapUnaryRecords = True }