----------------------------------------------------------------------------
-- |
-- Module : Prettyprinter.Data
-- Copyright : (c) Sergey Vinokurov 2018
-- License : Apache-2.0 (see LICENSE)
-- Maintainer : serg.foo@gmail.com
----------------------------------------------------------------------------
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE PatternGuards #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Prettyprinter.Data
( ppData
, ppDataSimple
) where
import Data.Data
import Data.Generics qualified
import Prettyprinter
import Prettyprinter qualified as PP
import Prettyprinter.Combinators
import Prettyprinter.MetaDoc
-- $setup
-- >>> :set -XDeriveDataTypeable
-- >>> :set -XImportQualifiedPost
-- >>> import Data.Data
-- >>> import Data.List.NonEmpty (NonEmpty(..))
-- >>> import Data.List.NonEmpty qualified as NonEmpty
-- >>> import Data.Map.Strict (Map)
-- >>> import Data.Map.Strict qualified as Map
--
-- >>> :{
-- data Test =
-- Foo Int [Int] Double (Maybe Test)
-- | Bar (String, Int, Int) (Map String Int) (Map String Int) (Maybe Test) (NonEmpty Int)
-- deriving (Data)
-- :}
-- | Prettyprint using 'Data.Data' instance.
--
-- >>> :{
-- test =
-- Bar
-- ("foo", 10, 20)
-- (Map.fromList (zip ["foo", "bar", "baz"] [1..]))
-- (Map.fromList (zip ["foo", "bar", "baz", "quux", "fizz", "buzz", "frob", "wat"] [1..]))
-- (Just
-- (Foo
-- 1
-- []
-- 3.14159265358979323846264338327950288
-- (Just
-- (Foo
-- 1
-- [2]
-- 2.71828182
-- (Just (Bar ("x", 1, 2) mempty mempty Nothing (NonEmpty.fromList [42])))))))
-- (NonEmpty.fromList [1..42])
-- :}
--
-- >>> ppData test
-- Bar
-- (foo, 10, 20)
-- {bar -> 2, baz -> 3, foo -> 1}
-- { bar -> 2
-- , baz -> 3
-- , buzz -> 6
-- , fizz -> 5
-- , foo -> 1
-- , frob -> 7
-- , quux -> 4
-- , wat -> 8
-- }
-- Just Foo
-- 1
-- {}
-- 3.141592653589793
-- Just (Foo 1 [2] 2.71828182 (Just (Bar (x, 1, 2) {} {} Nothing [42])))
-- [ 1
-- , 2
-- , 3
-- , 4
-- , 5
-- , 6
-- , 7
-- , 8
-- , 9
-- , 10
-- , 11
-- , 12
-- , 13
-- , 14
-- , 15
-- , 16
-- , 17
-- , 18
-- , 19
-- , 20
-- , 21
-- , 22
-- , 23
-- , 24
-- , 25
-- , 26
-- , 27
-- , 28
-- , 29
-- , 30
-- , 31
-- , 32
-- , 33
-- , 34
-- , 35
-- , 36
-- , 37
-- , 38
-- , 39
-- , 40
-- , 41
-- , 42
-- ]
ppData :: Data a => a -> Doc ann
ppData = mdPayload . gpretty
ppDataSimple :: Data a => a -> Doc ann
ppDataSimple = pretty . Data.Generics.gshow
gpretty :: forall a ann. Data a => a -> MetaDoc ann
gpretty =
render
`Data.Generics.extQ` stringMetaDoc
`Data.Generics.extQ` strictTextMetaDoc
`Data.Generics.extQ` lazyTextMetaDoc
`Data.Generics.extQ` metaDocInt
`Data.Generics.extQ` metaDocFloat
`Data.Generics.extQ` metaDocDouble
`Data.Generics.extQ` metaDocInteger
`Data.Generics.extQ` metaDocWord
`Data.Generics.extQ` metaDocWord8
`Data.Generics.extQ` metaDocWord16
`Data.Generics.extQ` metaDocWord32
`Data.Generics.extQ` metaDocWord64
`Data.Generics.extQ` metaDocInt8
`Data.Generics.extQ` metaDocInt16
`Data.Generics.extQ` metaDocInt32
`Data.Generics.extQ` metaDocInt64
`Data.Generics.extQ` metaDocUnit
`Data.Generics.extQ` metaDocBool
`Data.Generics.extQ` metaDocChar
-- Probably requires qualified constrtains...
-- `Data.Generics.extQ`
-- ((atomicMetaDoc . ppMapWith (mdPayload . gpretty) (mdPayload . gpretty)) ::
-- forall k v. (Data k, Data v) => Map k v -> MetaDoc ann)
where
render :: Data b => b -> MetaDoc ann
render t
| constructorName == "fromList"
, Just mapItems <- gmapQi 0 (listElements (isPair gpretty)) t
, Just mapItems' <- sequence mapItems
= atomicMetaDoc
$ ppAssocListWith mdPayload mdPayload mapItems'
| Just mapItems <- listElements (isPair gpretty) t
, Just mapItems' <- sequence mapItems
= atomicMetaDoc
$ ppAssocListWith mdPayload mdPayload mapItems'
| Just listItems <- listElements gpretty t
= atomicMetaDoc
$ ppListWith mdPayload listItems
| isTuple
= atomicMetaDoc
$ ppListWithDelim PP.lparen PP.rparen
$ map mdPayload fields
| otherwise
= constructorAppMetaDoc constructorDoc fields
where
constructorDoc :: MetaDoc ann
constructorDoc = atomicMetaDoc $ pretty constructorName
fields :: [MetaDoc ann]
fields = gmapQ gpretty t
constructorName :: String
constructorName = showConstr $ toConstr t
isTuple :: Bool
isTuple = all (== ',') (filter (not . (`elem` ("()" :: String))) constructorName)
isPair :: Data a => (forall b. Data b => b -> c) -> a -> Maybe (c, c)
isPair f x
| constructorName == "(,)" = Just (gmapQi 0 f x, gmapQi 1 f x)
| otherwise = Nothing
where
constructorName :: String
constructorName = showConstr $ toConstr x
-- | Try to treat @a@ as a list and prettyprint its elements with @f@.
-- Returns Just on succes and Nothing if @a@ wasn't a list after all.
listElements :: forall a c. Data a => (forall b. Data b => b -> c) -> a -> Maybe [c]
listElements f = go
where
go :: Data d => d -> Maybe [c]
go x
| isNull = Just []
| isCons = (:) (gmapQi 0 f x) <$> gmapQi 1 go x
| otherwise = Nothing
where
constructorName :: String
constructorName = showConstr $ toConstr x
isCons = constructorName `elem` ["(:)", ":|"]
isNull = constructorName == "[]"