packages feed

roundtrip-aeson-0.3.0.2: lib/Data/Aeson/Roundtrip.hs

{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedLists   #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes        #-}

-- TH does not generate signatures
{-# OPTIONS_GHC -fno-warn-missing-signatures #-}

module Data.Aeson.Roundtrip
(
    -- * Parser/Builder
    JSONParser(..),
    JSONBuilder(..),
    -- * Combinators
    is,
    wat,
    -- * Syntaxes
    jsonField,
    jsonString,
    jsonBool,
    jsonNumber,
    jsonIntegral,
    jsonRealFloat,
    -- * Lenses, Prisms, and Isomorphisms.
    demote,
    demoteLR,
    demoteL,
    demoteR,
    -- * JSON Syntax
    JSONSyntax (..)
)
where

import           Control.Category            ((.))
import           Control.Isomorphism.Partial
import           Control.Lens                hiding (Iso)
import qualified Control.Lens                as L
import           Control.Monad               (guard, liftM2, mplus, (>=>))
import           Data.Aeson
import           Data.Aeson.Lens
import           Data.HashMap.Strict         (union)
import           Data.Monoid
import           Data.Scientific
import           Data.Text                   (Text)
import           Data.Vector                 ((!?))
import qualified Data.Vector                 as V
import           Prelude                     hiding ((.), (<$>)) -- curse the symbols of tyrrany
import           Text.Roundtrip.Classes


-- | Demote a lens 'Prism' to a partial 'Iso'.
--
-- This involves strapping a _Just onto the review as a prism is slightly
-- "stronger" than an Iso anyway. Bear in mind that this is not a lens iso but
-- a RoundTrip iso.
--
-- This also works on lens isos, you can imagine this as :
--
--   demote :: L.Iso' a b -> Iso a b
--
demote :: String -> Prism' a b -> Iso a b
demote name p = unsafeMakeNamedIso name (preview p) (review (_Just . p))

-- | Demote something with show instances for better messages.
demoteLR :: (Show a, Show b) => String -> Prism' a b -> Iso a b
demoteLR name p = unsafeMakeNamedIsoLR name (preview p) (review (_Just . p))

demoteL :: Show a => String -> Prism' a b -> Iso a b
demoteL name p = unsafeMakeNamedIsoL name (preview p) (review (_Just . p))

demoteR :: Show b => String -> Prism' a b -> Iso a b
demoteR name p = unsafeMakeNamedIsoR name (preview p) (review (_Just . p))

-- | Parse and unparse JSON values.
class Syntax s => JSONSyntax s where

    -- | Run a parser over some other parser.
    --
    -- This can be used to, e.g., traverse the fields of an 'Object'
    -- constructor and parse the result.
    runSub :: s v -> s Value -> s v

    -- | Parse any JSON value.
    value :: s Value

-- | Ensure that a value 'a' is "produced" or "consumed".
--
-- This is intended to be used infix in conjunction with *> and <*
is :: (JSONSyntax s, Eq a) => s a -> a -> s ()
is s a = demoteR "is" (prism' (const a) (guard . (a ==))) <$> s

-- | With Arbitrary Thing: Given a thing, ensure that it is always included on
-- the way "back" from JSON, but never ends up in the JSON document.
--
-- This is almost like pure, going one way.
wat :: JSONSyntax s => a -> s a
wat a = demoteL "wat"
                (prism' (const $ Object mempty) (const $ Just a)) <$> value

-- | Un-/parse from within a field in a JSON object.
jsonField
    :: JSONSyntax s
    => Text
    -- ^ Key to lookup/insert
    -> s v
    -- ^ Sub-parser
    -> s v
jsonField k syntax = runSub syntax (keyIso <$> value)
  where
    -- Only valid if we assume that isomorphism is viewed from the non-JSON end
    -- of things. This forgets any context.
    keyIso = demoteLR ("key " <> show k) $ prism' (\part -> Object [(k,part)]) (^? key k)

-- | Un-/parse a boolean JSON value.
jsonBool :: JSONSyntax s => s Bool
jsonBool = demoteLR "jsonBool" _Bool <$> value

-- | Un-/parse a number JSON value.
jsonNumber :: JSONSyntax s => s Scientific
jsonNumber = demoteLR "jsonNumber" _Number <$> value

-- | Un-/parse an integral number JSON value.
jsonIntegral :: (Integral a, JSONSyntax s) => s a
jsonIntegral = demoteL "jsonIntegral" _Integral <$> value

-- | Un-/parse a floating number JSON value.
jsonRealFloat :: (RealFloat a, JSONSyntax s) => s a
jsonRealFloat = i . demoteL "jsonRealFloat (number)" _Number <$> value
  where
    i = demoteL "jsonRealFloat (toRealFloat)" $
            L.iso toRealFloat (fromRational . toRational)

-- | Un-/parse a string JSON value.
jsonString :: JSONSyntax s => s Text
jsonString = demoteLR "String" _String <$> value

-- | Try to apply an iso, provide message on failure
tryLR :: Iso a b -> a -> Either String b
tryLR i a =
    case isoLR i a of
        Just x -> Right x
        Nothing -> Left $ isoFailedErrorMessageL i a

-- | Try to unapply an iso, provide message on failure
tryRL :: Iso a b -> b -> Either String a
tryRL i b =
    case isoRL i b of
        Just x -> Right x
        Nothing -> Left $ isoFailedErrorMessageR i b

-- | An implementation of 'JSONSyntax' which constructs JSON values.
newtype JSONBuilder a = JSONBuilder
    { runBuilder :: a -> Either String Value }

instance IsoFunctor JSONBuilder where
    -- When going from a to 'Value' we simply want to compose the possible iso
    -- failures in the 'unapply' direction.
    i <$> JSONBuilder b = JSONBuilder $ tryRL i >=> b

instance ProductFunctor JSONBuilder where
    -- When building a 'Value' we want to decompose our church pair list tupled
    -- builders and merge the results together.
    --
    -- Note that the second argument is not pattern matched, this is to ensure
    -- that it is not eagerly constructed and does not diverge in things like
    -- many.
    JSONBuilder p <*> JSONBuilder q = JSONBuilder $ \(a,b) -> do
        a' <- p a
        b' <- q b
        merge a' b'
      where
        -- Merging of two objects is simply a union, this rule fires when you
        -- do things like:
        --
        -- jsonField "a" p <*> jsonField "b" p
        merge (Object a) (Object b) = Right . Object $ a `union` b
        -- merge Null (Object b) = Object b
        -- Merging of head and tail of arrays, this rule fires when using
        -- things like the many combinator to create a JSON array
        merge a (Array b) = Right . Array $ V.cons a b
        merge x Null = Right x
        merge Null x = Right x
        merge x y = Left $
            "Don't know how to merge: " <> show x <> " <*> " <> show y

instance Alternative JSONBuilder where
    -- Try the left first, then right.
    JSONBuilder p <||> JSONBuilder q = JSONBuilder $ \a -> p a `mplus` q a

    -- Always Left
    empty = JSONBuilder . const $ Left "empty"

instance Syntax JSONBuilder where
    -- | Have to rewrite Null as [] as pure () is will make a Null as it
    -- terminates the list.
    --
    -- This is so that pure can make nulls, which is "nicer" for things like
    -- optional.
    rule "many" _ (JSONBuilder b) =
        JSONBuilder $ b >=> (\case Null -> Right $ Array mempty
                                   x    -> Right x)
    rule _ _ x = x

    pure x = JSONBuilder $ \y ->
        if x == y
            then Right Null
            else Left "pure, x /= y"

instance JSONSyntax JSONBuilder where
    -- | To roduces a 'Value', we simply need to pass it through.
    value = JSONBuilder Right

    -- Run a sub-parser. Just composition, really.
    runSub (JSONBuilder a) (JSONBuilder b) =
        JSONBuilder $ a >=> b


-- | An implementation of 'JSONSyntax' which deconstructs JSON values.
newtype JSONParser a = JSONParser
    { runParser :: Value -> Either String a }

instance IsoFunctor JSONParser where
    -- The opposite of a JSONParser in both order of composition and direction
    -- of iso
    i <$> JSONParser p = JSONParser $ p >=> tryLR i

instance ProductFunctor JSONParser where
    -- When coming from a 'Value' we either want to tuple things up, or, in
    -- the special case of a list, consume the head and pass the tail on. This
    -- is a simple way of getting the many combinator to work on JSON.
    JSONParser p <*> JSONParser q = JSONParser f
      where
        f v | Array x <- v, Just y <- x !? 0
            = liftM2 (,) (p y) (q . Array $ V.tail x)
            | Array _ <- v
            = Left "Empty array"
            | otherwise
            = liftM2 (,) (p v) (q v)

instance Alternative JSONParser where
    JSONParser p <||> JSONParser q = JSONParser $ \v -> p v `mplus` q v

    empty = JSONParser . const $ Left "empty"

instance Syntax JSONParser where
    pure = JSONParser . const . Right

instance JSONSyntax JSONParser where
    value = JSONParser Right

    runSub (JSONParser a) (JSONParser b) = JSONParser $ b >=> a