purescript-bridge-0.15.0.0: src/Language/PureScript/Bridge/Printer.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE LambdaCase #-}
module Language.PureScript.Bridge.Printer where
import Control.Lens
import Control.Monad
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Monoid ((<>))
import Data.Set (Set)
import Data.Maybe (isJust)
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.IO as T
import System.Directory
import System.FilePath
import Language.PureScript.Bridge.SumType
import Language.PureScript.Bridge.TypeInfo
import qualified Language.PureScript.Bridge.CodeGenSwitches as Switches
data Module (lang :: Language) = PSModule {
psModuleName :: !Text
, psImportLines :: !(Map Text ImportLine)
, psTypes :: ![SumType lang]
} deriving Show
type PSModule = Module 'PureScript
data ImportLine = ImportLine {
importModule :: !Text
, importAlias :: !(Maybe Text)
, importTypes :: !(Set Text)
} deriving Show
type Modules = Map Text PSModule
type ImportLines = Map Text ImportLine
printModule :: Switches.Settings -> FilePath -> PSModule -> IO ()
printModule settings root m = do
unlessM (doesDirectoryExist mDir) $ createDirectoryIfMissing True mDir
T.writeFile mPath . moduleToText settings $ m
where
mFile = (joinPath . map T.unpack . T.splitOn "." $ psModuleName m) <> ".purs"
mPath = root </> mFile
mDir = takeDirectory mPath
sumTypesToNeededPackages :: [SumType lang] -> Set Text
sumTypesToNeededPackages = Set.unions . map sumTypeToNeededPackages
sumTypeToNeededPackages :: SumType lang -> Set Text
sumTypeToNeededPackages st =
Set.filter (not . T.null) . Set.map _typePackage $ getUsedTypes st
moduleToText :: Switches.Settings -> Module 'PureScript -> Text
moduleToText settings m = T.unlines $
"-- File auto generated by purescript-bridge! --"
: "module " <> psModuleName m <> " where\n"
: map importLineToText allImports
<> [ ""
, "import Prelude"
, ""
]
<> map (sumTypeToText settings) (psTypes m)
where
otherImports = importsFromList $
_lensImports settings
<> _genericsImports settings
<> _argonautCodecsImports settings
<> _foreignImports settings
allImports = Map.elems $ mergeImportLines otherImports (psImportLines m)
_genericsImports :: Switches.Settings -> [ImportLine]
_genericsImports settings
| Switches.genericsGenRep settings =
[ ImportLine "Data.Generic.Rep" Nothing $ Set.fromList ["class Generic"] ]
| otherwise =
[ ImportLine "Data.Generic" Nothing $ Set.fromList ["class Generic"] ]
_lensImports :: Switches.Settings -> [ImportLine]
_lensImports settings
| Switches.generateLenses settings =
[ ImportLine "Data.Lens" Nothing $ Set.fromList ["Iso'", "Prism'", "Lens'", "prism'", "lens"]
, ImportLine "Data.Lens.Iso.Newtype" Nothing $ Set.fromList ["_Newtype"]
, ImportLine "Data.Lens.Record" Nothing $ Set.fromList ["prop"]
] <> baseline <>
[ ImportLine "Type.Proxy" Nothing $ Set.fromList ["Proxy(Proxy)"]
]
| otherwise = baseline
where
baseline =
[ ImportLine "Data.Maybe" Nothing $ Set.fromList ["Maybe(..)"]
, ImportLine "Data.Newtype" Nothing $ Set.fromList ["class Newtype"]
]
_argonautCodecsImports :: Switches.Settings -> [ImportLine]
_argonautCodecsImports settings
| Switches.generateArgonautCodecs settings =
[ ImportLine "Data.Argonaut.Aeson.Decode.Generic" Nothing $ Set.fromList [ "genericDecodeAeson" ]
, ImportLine "Data.Argonaut.Aeson.Encode.Generic" Nothing $ Set.fromList [ "genericEncodeAeson" ]
, ImportLine "Data.Argonaut.Aeson.Options" (Just "Argonaut") $ Set.fromList [ "defaultOptions" ]
, ImportLine "Data.Argonaut.Decode.Class" Nothing $ Set.fromList [ "class DecodeJson", "class DecodeJsonField", "decodeJson" ]
, ImportLine "Data.Argonaut.Encode.Class" Nothing $ Set.fromList [ "class EncodeJson", "encodeJson" ]
]
| otherwise = mempty
_foreignImports :: Switches.Settings -> [ImportLine]
_foreignImports settings
| (isJust . Switches.generateForeign) settings =
[ ImportLine "Foreign.Class" Nothing $ Set.fromList ["class Decode", "class Encode"]
, ImportLine "Foreign.Generic" Nothing $ Set.fromList ["defaultOptions", "genericDecode", "genericEncode"]
]
| otherwise = mempty
importLineToText :: ImportLine -> Text
importLineToText = \case
ImportLine importModule Nothing importTypes ->
"import " <> importModule <> " (" <> typeList importTypes <> ")"
ImportLine importModule (Just importAlias) _ ->
"import " <> importModule <> " as " <> importAlias
where
typeList s = T.intercalate ", " (Set.toList s)
sumTypeToText :: Switches.Settings -> SumType 'PureScript -> Text
sumTypeToText settings st =
sumTypeToTypeDecls settings st <> additionalCode
where
additionalCode =
if Switches.generateLenses settings then lenses else mempty
lenses = "\n" <> sep <> "\n" <> sumTypeToOptics st <> sep
sep = T.replicate 80 "-"
sumTypeToTypeDecls :: Switches.Settings -> SumType 'PureScript -> Text
sumTypeToTypeDecls settings (SumType t cs is) = T.unlines $
dataOrNewtype <> " " <> typeInfoToText True t <> " ="
: " " <> T.intercalate "\n | " (map (constructorToText 4) cs) <> "\n"
: instances settings (SumType t cs (filter genForeign . filter genArgonautCodec $ is))
where
dataOrNewtype = if isJust (nootype cs) then "newtype" else "data"
genForeign :: Instance -> Bool
genForeign = \case
Encode -> check
Decode -> check
_ -> True
where check = (isJust . Switches.generateForeign) settings
genArgonautCodec :: Instance -> Bool
genArgonautCodec = \case
EncodeJson -> check
DecodeJson -> check
_ -> True
where check = Switches.generateArgonautCodecs settings
foreignOptionsToPurescript :: Maybe Switches.ForeignOptions -> Text
foreignOptionsToPurescript = \case
Nothing -> mempty
Just (Switches.ForeignOptions{..}) ->
" { unwrapSingleConstructors = "
<> (T.toLower . T.pack . show $ unwrapSingleConstructors)
<> " , unwrapSingleArguments = "
<> (T.toLower . T.pack . show $ unwrapSingleArguments)
<> " }"
-- | Given a Purescript type, generate instances for typeclass
-- instances it claims to have.
instances :: Switches.Settings -> SumType 'PureScript -> [Text]
instances settings st@(SumType t _ is) = map go is
where
go :: Instance -> Text
go Encode = "instance encode" <> _typeName t <> " :: " <> extras <> "Encode " <> typeInfoToText False t <> " where\n" <>
" encode = genericEncode $ defaultOptions" <> encodeOpts
where
encodeOpts =
foreignOptionsToPurescript $ Switches.generateForeign settings
stpLength = length sumTypeParameters
extras | stpLength == 0 = mempty
| otherwise = bracketWrap constraintsInner <> " => "
sumTypeParameters = filter (isTypeParam t) . Set.toList $ getUsedTypes st
constraintsInner = T.intercalate ", " $ map instances sumTypeParameters
instances params = genericInstance settings params <> ", " <> encodeInstance params
bracketWrap x = "(" <> x <> ")"
go EncodeJson = "instance encodeJson" <> _typeName t <> " :: " <> extras <> "EncodeJson " <> typeInfoToText False t <> " where\n" <>
" encodeJson = genericEncodeAeson Argonaut.defaultOptions"
where
encodeOpts =
foreignOptionsToPurescript $ Switches.generateForeign settings
stpLength = length sumTypeParameters
extras | stpLength == 0 = mempty
| otherwise = bracketWrap constraintsInner <> " => "
sumTypeParameters = filter (isTypeParam t) . Set.toList $ getUsedTypes st
constraintsInner = T.intercalate ", " $ map instances sumTypeParameters
instances params = genericInstance settings params <> ", " <> encodeJsonInstance params
bracketWrap x = "(" <> x <> ")"
go Decode = "instance decode" <> _typeName t <> " :: " <> extras <> "Decode " <> typeInfoToText False t <> " where\n" <>
" decode = genericDecode $ defaultOptions" <> decodeOpts
where
decodeOpts =
foreignOptionsToPurescript $ Switches.generateForeign settings
stpLength = length sumTypeParameters
extras | stpLength == 0 = mempty
| otherwise = bracketWrap constraintsInner <> " => "
sumTypeParameters = filter (isTypeParam t) . Set.toList $ getUsedTypes st
constraintsInner = T.intercalate ", " $ map instances sumTypeParameters
instances params = genericInstance settings params <> ", " <> decodeInstance params
bracketWrap x = "(" <> x <> ")"
go DecodeJson = "instance decodeJson" <> _typeName t <> " :: " <> extras <> "DecodeJson " <> typeInfoToText False t <> " where\n" <>
" decodeJson = genericDecodeAeson Argonaut.defaultOptions"
where
stpLength = length sumTypeParameters
extras | stpLength == 0 = mempty
| otherwise = bracketWrap constraintsInner <> " => "
sumTypeParameters = filter (isTypeParam t) . Set.toList $ getUsedTypes st
constraintsInner = T.intercalate ", " $ map instances sumTypeParameters
instances params = genericInstance settings params <> ", " <> decodeJsonInstance params <> ", " <> decodeJsonFieldInstance params
bracketWrap x = "(" <> x <> ")"
go i = "derive instance " <> T.toLower c <> _typeName t <> " :: " <> extras i <> c <> " " <> typeInfoToText False t <> postfix i
where c = T.pack $ show i
extras Generic | stpLength == 0 = mempty
| stpLength == 1 = genericConstraintsInner <> " => "
| otherwise = bracketWrap genericConstraintsInner <> " => "
extras _ = ""
postfix Newtype = " _"
postfix Generic
| Switches.genericsGenRep settings = " _"
| otherwise = ""
postfix _ = ""
stpLength = length sumTypeParameters
sumTypeParameters = filter (isTypeParam t) . Set.toList $ getUsedTypes st
genericConstraintsInner = T.intercalate ", " $ map (genericInstance settings) sumTypeParameters
bracketWrap x = "(" <> x <> ")"
isTypeParam :: PSType -> PSType -> Bool
isTypeParam t typ = _typeName typ `elem` map _typeName (_typeParameters t)
encodeInstance :: PSType -> Text
encodeInstance params = "Encode " <> typeInfoToText False params
encodeJsonInstance :: PSType -> Text
encodeJsonInstance params = "EncodeJson " <> typeInfoToText False params
decodeInstance :: PSType -> Text
decodeInstance params = "Decode " <> typeInfoToText False params
decodeJsonInstance :: PSType -> Text
decodeJsonInstance params = "DecodeJson " <> typeInfoToText False params
decodeJsonFieldInstance :: PSType -> Text
decodeJsonFieldInstance params = "DecodeJsonField " <> typeInfoToText False params
genericInstance :: Switches.Settings -> PSType -> Text
genericInstance settings params =
if not (Switches.genericsGenRep settings) then
"Generic " <> typeInfoToText False params
else
"Generic " <> typeInfoToText False params <> " r" <> mergedTypeInfoToText params
sumTypeToOptics :: SumType 'PureScript -> Text
sumTypeToOptics st = constructorOptics st <> recordOptics st
constructorOptics :: SumType 'PureScript -> Text
constructorOptics st =
case st ^. sumTypeConstructors of
[] -> mempty -- No work required.
[c] -> constructorToOptic False typeInfo c
cs -> T.unlines $ map (constructorToOptic True typeInfo) cs
where
typeInfo = st ^. sumTypeInfo
recordOptics :: SumType 'PureScript -> Text
-- Match on SumTypes with a single DataConstructor (that's a list of a single element)
recordOptics st@(SumType _ [_] _) = T.unlines $ recordEntryToLens st <$> dcRecords
where
cs = st ^. sumTypeConstructors
dcRecords = lensableConstructor ^.. traversed.sigValues._Right.traverse.filtered hasUnderscore
hasUnderscore e = e ^. recLabel.to (T.isPrefixOf "_")
lensableConstructor = filter singleRecordCons cs ^? _head
singleRecordCons (DataConstructor _ (Right _)) = True
singleRecordCons _ = False
recordOptics _ = ""
constructorToText :: Int -> DataConstructor 'PureScript -> Text
constructorToText _ (DataConstructor n (Left [])) = n
constructorToText _ (DataConstructor n (Left ts)) = n <> " " <> T.intercalate " " (map (typeInfoToText False) ts)
constructorToText indentation (DataConstructor n (Right rs)) =
n <> " {\n"
<> spaces (indentation + 2) <> T.intercalate intercalation (map recordEntryToText rs) <> "\n"
<> spaces indentation <> "}"
where
intercalation = "\n" <> spaces indentation <> "," <> " "
spaces :: Int -> Text
spaces c = T.replicate c " "
typeNameAndForall :: TypeInfo 'PureScript -> (Text, Text)
typeNameAndForall typeInfo = (typName, forAll)
where
typName = typeInfoToText False typeInfo
forAllParams = typeInfo ^.. typeParameters.traversed.to (typeInfoToText False)
forAll = case forAllParams of
[] -> " :: "
cs -> " :: forall " <> T.intercalate " " cs <> ". "
fromEntries :: (RecordEntry a -> Text) -> [RecordEntry a] -> Text
fromEntries mkElem rs = "{ " <> inners <> " }"
where
inners = T.intercalate ", " $ map mkElem rs
mkFnArgs :: [RecordEntry 'PureScript] -> Text
mkFnArgs [r] = r ^. recLabel
mkFnArgs rs = fromEntries (\recE -> recE ^. recLabel <> ": " <> recE ^. recLabel) rs
mkTypeSig :: [RecordEntry 'PureScript] -> Text
mkTypeSig [] = "Unit"
mkTypeSig [r] = typeInfoToText False $ r ^. recValue
mkTypeSig rs = fromEntries recordEntryToText rs
constructorToOptic :: Bool -> TypeInfo 'PureScript -> DataConstructor 'PureScript -> Text
constructorToOptic otherConstructors typeInfo (DataConstructor n args) =
case (args,otherConstructors) of
(Left [c], False) ->
pName <> forAll <> "Iso' " <> typName <> " " <> mkTypeSig (constructorTypes [c]) <> "\n"
<> pName <> " = _Newtype"
<> "\n"
(Left cs, _) ->
pName <> forAll <> "Prism' " <> typName <> " " <> mkTypeSig types <> "\n"
<> pName <> " = prism' " <> getter <> " f\n"
<> spaces 2 <> "where\n"
<> spaces 4 <> "f " <> mkF cs
<> otherConstructorFallThrough
<> "\n"
where
mkF [] = n <> " = Just unit\n"
mkF _ = "(" <> n <> " " <> T.unwords (map _recLabel types) <> ") = Just $ " <> mkFnArgs types <> "\n"
getter | null cs = "(\\_ -> " <> n <> ")"
| length cs == 1 = n
| otherwise = "(\\{ " <> T.intercalate ", " cArgs <> " } -> " <> n <> " " <> T.intercalate " " cArgs <> ")"
where
cArgs = map (T.singleton . fst) $ zip ['a'..] cs
types = constructorTypes cs
(Right rs, False) ->
pName <> forAll <> "Iso' " <> typName <> " { " <> recordSig rs <> "}\n"
<> pName <> " = _Newtype\n"
<> "\n"
(Right rs, True) ->
pName <> forAll <> "Prism' " <> typName <> " { " <> recordSig rs <> " }\n"
<> pName <> " = prism' " <> n <> " f\n"
<> spaces 2 <> "where\n"
<> spaces 4 <> "f (" <> n <> " r) = Just r\n"
<> otherConstructorFallThrough
<> "\n"
where
recordSig rs = T.intercalate ", " (map recordEntryToText rs)
constructorTypes cs = [RecordEntry (T.singleton label) t | (label, t) <- zip ['a'..] cs]
(typName, forAll) = typeNameAndForall typeInfo
pName = "_" <> n
otherConstructorFallThrough | otherConstructors = spaces 4 <> "f _ = Nothing"
| otherwise = ""
recordEntryToLens :: SumType 'PureScript -> RecordEntry 'PureScript -> Text
recordEntryToLens st e =
if hasUnderscore
then lensName <> forAll <> "Lens' " <> typName <> " " <> recType <> "\n"
<> lensName <> " = _Newtype <<< prop (Proxy :: Proxy \"" <> recName <> "\")\n"
else ""
where
(typName, forAll) = typeNameAndForall (st ^. sumTypeInfo)
recName = e ^. recLabel
lensName = T.drop 1 recName
recType = typeInfoToText False (e ^. recValue)
hasUnderscore = e ^. recLabel.to (T.isPrefixOf "_")
recordEntryToText :: RecordEntry 'PureScript -> Text
recordEntryToText e = _recLabel e <> " :: " <> typeInfoToText True (e ^. recValue)
typeInfoToText :: Bool -> PSType -> Text
typeInfoToText topLevel t = if needParens then "(" <> inner <> ")" else inner
where
inner = _typeName t <>
if pLength > 0
then " " <> T.intercalate " " textParameters
else ""
params = _typeParameters t
pLength = length params
needParens = not topLevel && pLength > 0
textParameters = map (typeInfoToText False) params
mergedTypeInfoToText :: PSType -> Text
mergedTypeInfoToText t =
_typeName t <> T.concat textParameters
where
params = _typeParameters t
textParameters = map mergedTypeInfoToText params
sumTypesToModules :: Modules -> [SumType 'PureScript] -> Modules
sumTypesToModules = foldr sumTypeToModule
sumTypeToModule :: SumType 'PureScript -> Modules -> Modules
sumTypeToModule st@(SumType t _ _) = Map.alter (Just . updateModule) (_typeModule t)
where
updateModule Nothing = PSModule {
psModuleName = _typeModule t
, psImportLines = dropSelf $ typesToImportLines Map.empty (getUsedTypes st)
, psTypes = [st]
}
updateModule (Just m) = m {
psImportLines = dropSelf $ typesToImportLines (psImportLines m) (getUsedTypes st)
, psTypes = st : psTypes m
}
dropSelf = Map.delete (_typeModule t)
typesToImportLines :: ImportLines -> Set PSType -> ImportLines
typesToImportLines = foldr typeToImportLines
typeToImportLines :: PSType -> ImportLines -> ImportLines
typeToImportLines t ls = typesToImportLines (update ls) (Set.fromList (_typeParameters t))
where
update = if not (T.null (_typeModule t))
then Map.alter (Just . updateLine) (_typeModule t)
else id
updateLine Nothing = ImportLine (_typeModule t) Nothing (Set.singleton (_typeName t))
updateLine (Just (ImportLine m alias types)) =
ImportLine m alias (Set.insert (_typeName t) types)
importsFromList :: [ImportLine] -> Map Text ImportLine
importsFromList ls = let
pairs = zip (map importModule ls) ls
merge a b = ImportLine (importModule a) (importAlias a) (importTypes a `Set.union` importTypes b)
in
Map.fromListWith merge pairs
mergeImportLines :: ImportLines -> ImportLines -> ImportLines
mergeImportLines = Map.unionWith mergeLines
where
mergeLines a b = ImportLine (importModule a) (importAlias a) (importTypes a `Set.union` importTypes b)
unlessM :: Monad m => m Bool -> m () -> m ()
unlessM mbool action = mbool >>= flip unless action