waargonaut-0.6.0.0: src/Waargonaut/Types/JString.hs
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE TypeFamilies #-}
-- | Types and functions for handling JSON strings.
module Waargonaut.Types.JString
(
-- * Types
JString
, JString' (..)
, AsJString (..)
, _JStringText
, stringToJString
-- * Parser
, parseJString
) where
import Prelude (Eq, Ord, Show, String, foldr)
import Control.Applicative (Applicative, (*>), (<*))
import Control.Category (id, (.))
import Control.Error.Util (note)
import Control.Lens (Prism', Profunctor, Rewrapped,
Wrapped (..), iso, prism, review)
import Data.Either (Either (Right))
import Data.Foldable (Foldable)
import Data.Function (($))
import Data.Functor (Functor, fmap, (<$>))
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Traversable (Traversable, traverse)
import Data.Vector (Vector)
import qualified Data.Vector as V
import Data.Digit (HeXDigit)
import Text.Parser.Char (CharParsing, char)
import Text.Parser.Combinators (many)
import Waargonaut.Types.JChar (JChar, charToJChar, jCharToChar,
parseJChar, utf8CharToJChar)
-- $setup
-- >>> :set -XOverloadedStrings
-- >>> import Control.Lens ((#))
-- >>> import Control.Monad (return)
-- >>> import Data.Function (($))
-- >>> import Data.Either(Either (..), isLeft)
-- >>> import Data.Digit (HeXDigit(..))
-- >>> import qualified Data.Vector as V
-- >>> import Utils
-- >>> import Waargonaut.Decode.Error (DecodeError)
-- >>> import Waargonaut.Types.Whitespace
-- >>> import Waargonaut.Types.JChar.Unescaped
-- >>> import Waargonaut.Types.JChar.Escaped
-- >>> import Waargonaut.Types.JChar.HexDigit4
-- >>> import Waargonaut.Types.JChar
-- >>> import Data.Text.Lazy.Builder (toLazyText)
----
-- | A JSON string is a list of JSON acceptable characters, we use a newtype to
-- create the 'JString' type from a 'Vector JChar'. This is polymorphic over the
-- acceptable types of character encoding digits.
newtype JString' digit =
JString' (Vector (JChar digit))
deriving (Eq, Ord, Show, Functor, Foldable, Traversable)
-- | As only one subset of digits are currently acceptable, Hexadecimal, we
-- provide this type alias to close that loop.
type JString = JString' HeXDigit
instance JString' digit ~ t => Rewrapped (JString' digit) t
instance Wrapped (JString' digit) where
type Unwrapped (JString' digit) = Vector (JChar digit)
_Wrapped' = iso (\ (JString' x) -> x) JString'
-- | Classy 'Control.Lens.Prism'' for things that may be treated as a 'JString'.
class AsJString a where
_JString :: Prism' a JString
instance AsJString JString where
_JString = id
instance AsJString [JChar HeXDigit] where
_JString = prism (\(JString' cs) -> V.toList cs) (Right . JString' . V.fromList)
instance AsJString String where
_JString = prism
(\(JString' cx) -> V.toList $ jCharToChar <$> cx)
(\x -> JString' . V.fromList <$> traverse (note x . charToJChar) x)
-- | Conversion between a 'JString' and 'Text'.
--
-- JSON strings a wider range of encodings than 'Text' and to be consistent with
-- the 'Text' type, these invalid types are replaced with a placeholder value.
--
_JStringText :: (Profunctor p, Applicative f) => p Text (f Text) -> p JString (f JString)
_JStringText = iso (Text.pack . review _JString) (JString' . V.fromList . fmap utf8CharToJChar . Text.unpack)
-- | Parse a 'JString', storing escaped characters and any explicitly escaped
-- character encodings '\uXXXX'.
--
-- >>> testparse parseJString "\"\""
-- Right (JString' [])
--
-- >>> testparse parseJString "\"\\\\\""
-- Right (JString' [EscapedJChar ReverseSolidus])
--
-- >>> testparse parseJString "\"abc\""
-- Right (JString' [UnescapedJChar (Unescaped 'a'),UnescapedJChar (Unescaped 'b'),UnescapedJChar (Unescaped 'c')])
--
-- >>> testparse parseJString "\"a\\rbc\""
-- Right (JString' [UnescapedJChar (Unescaped 'a'),EscapedJChar (WhiteSpace CarriageReturn),UnescapedJChar (Unescaped 'b'),UnescapedJChar (Unescaped 'c')])
--
-- >>> testparse parseJString "\"a\\rbc\\uab12\\ndef\\\"\"" :: Either DecodeError JString
-- Right (JString' [UnescapedJChar (Unescaped 'a'),EscapedJChar (WhiteSpace CarriageReturn),UnescapedJChar (Unescaped 'b'),UnescapedJChar (Unescaped 'c'),EscapedJChar (Hex (HexDigit4 HeXDigita HeXDigitb HeXDigit1 HeXDigit2)),EscapedJChar (WhiteSpace NewLine),UnescapedJChar (Unescaped 'd'),UnescapedJChar (Unescaped 'e'),UnescapedJChar (Unescaped 'f'),EscapedJChar QuotationMark])
--
-- >>> testparsethennoteof parseJString "\"a\"\\u"
-- Right (JString' [UnescapedJChar (Unescaped 'a')])
--
-- >>> testparsethennoteof parseJString "\"a\"\t"
-- Right (JString' [UnescapedJChar (Unescaped 'a')])
parseJString
:: CharParsing f
=> f JString
parseJString =
char '"' *> (JString' . V.fromList <$> many parseJChar) <* char '"'
-- | Convert a 'String' to a 'JString'.
stringToJString :: String -> JString
stringToJString = JString' . foldr (V.cons . utf8CharToJChar) V.empty