cdp-0.0.1.0: gen/CDP/Gen/Program.hs
{-# LANGUAGE OverloadedStrings, FlexibleContexts, TupleSections, TypeOperators #-}
module CDP.Gen.Program
( Program (..)
, genProgram
, genProtocolModule
, ComponentName (..)
) where
import Control.Arrow
import Data.List
import Data.Maybe
import Data.Char
import Control.Monad
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.Graph as Graph
import qualified Data.Text as T
import qualified CDP.Definition as D
import qualified CDP.Gen.Snippets as Snippets
----------- Start of program generation -----------
data Program = Program
{ pComponents :: Map.Map ComponentName T.Text
, pComponentImports :: T.Text
}
data Context = Context { ctxDomainComponents :: DomainComponents }
genProgram :: [D.Domain] -> Program
genProgram delts = Program
{ pComponents = allComponents ctx $ Map.elems dc
, pComponentImports = T.unlines $ allComponentImports ctx
}
where
ctx = Context dc
dc = domainComponents delts
----------- Generation of global types / values -----------
genProtocolModule :: [ComponentName] -> T.Text -> T.Text
genProtocolModule names source = T.unlines
[ "{-# LANGUAGE OverloadedStrings, RecordWildCards, TupleSections #-}"
, "{-# LANGUAGE ScopedTypeVariables #-}"
, "{-# LANGUAGE FlexibleContexts #-}"
, "{-# LANGUAGE MultiParamTypeClasses #-}"
, "{-# LANGUAGE FlexibleInstances #-}"
, "{-# LANGUAGE DeriveGeneric #-}"
, ""
, protocolModuleHeader names
, Snippets.domainImports
, source
]
allComponentImports :: Context -> [T.Text]
allComponentImports = Set.toList . Set.fromList .
concatMap ((\n -> [importDomain False n, importDomain True n]) . unComponentName . cName) . Map.elems . ctxDomainComponents
allComponents :: Context -> [Component] -> Map.Map ComponentName T.Text
allComponents ctx components = Map.fromList .
map (cName &&& genComponent ctx) $
components
----------- Generation of domain types / values -----------
genComponent :: Context -> Component -> T.Text
genComponent ctx component = T.intercalate "\n\n" $
[ Snippets.domainLanguageExtensions
, formatComponentDescription component
, domainModuleHeader cn
, Snippets.domainImports
, T.unlines $ map (importDomain True) deps
, T.unlines $ map (genDomain ctx) delts
]
where
deps = Set.toList . Set.fromList . map unComponentName .
map (domainToComponentName ctx) . concatMap snd . Map.elems $ cDomDeps component
delts = map fst . Map.elems $ cDomDeps component
cn = unComponentName . cName $ component
genDomain :: Context -> D.Domain -> T.Text
genDomain ctx domainElt = T.unlines . intercalate [""] $
map (genType ctx dn) (D.domainTypes domainElt) ++
map (genEventReturnType ctx dn) (D.domainEvents domainElt) ++
map (genCommand ctx dn) (D.domainCommands domainElt)
where
dn = domainName domainElt
genType :: Context -> T.Text -> D.Type -> [T.Text]
genType ctx domainName telt =
desc ++
(case D.typeEnum telt of
Just enumValues -> genTypeEnum ctx tn enumValues
Nothing -> case tytelt of
"object" -> case tpeltsM of
Nothing -> [genTypeSynonynm ctx domainName tn]
Just tpelts ->
genRecordType ctx domainName tn tpelts ++
genRecordFromJson tn tpelts ++
genRecordToJson tn tpelts
ty -> [T.unwords ["type", tn, "=", lty]])
where
desc = formatDescription . Just $
"Type '" <> domainName <> "." <> D.typeId telt <> "'." <>
maybe "" (\x -> "\n" <> x) (D.typeDescription telt)
lty = leftType ctx domainName (Just tytelt) Nothing (D.typeItems telt)
tytelt = D.typeType telt
tpeltsM = D.typeProperties telt
tn = typeNameHS domainName telt
genTypeEnum :: Context -> T.Text -> [T.Text] -> [T.Text]
genTypeEnum _ typeEnumName values =
[T.unwords ["data", typeEnumName, "=", T.intercalate " | " constructors]] ++
indent [derivingOrd] ++
genFromJSONInstanceEnum typeEnumName values constructors ++
genToJSONInstanceEnum typeEnumName values constructors
where
constructors = map (tyNameHS typeEnumName) values
genEventReturnType :: Context -> T.Text -> D.Event -> [T.Text]
genEventReturnType ctx domainName eventElt =
desc ++
(case evelts of
[] -> ["data " <> evrn <> " = " <> evrn] ++ indent [derivingBase]
_ -> genRecordType ctx domainName evrn evelts) ++
(genRecordFromJson evrn evelts) ++
["instance Event " <> evrn <> " where"] ++
indent ["eventName _ = \"" <> eventName domainName eventElt <> "\""]
where
desc = formatDescription . Just $ "Type of the '" <>
(eventName domainName eventElt) <> "' event."
evelts = D.eventParameters eventElt
evrn = eventNameHS domainName eventElt
genCommand :: Context -> T.Text -> D.Command -> [T.Text]
genCommand ctx domainName commandElt =
formatDescription (D.commandDescription commandElt) ++
[""] ++
formatDescription (Just $ "Parameters of the '" <> cn <> "' command.") ++
genRecordType ctx domainName ptn pelts ++
genRecordSmartConstructor ctx domainName ptn pelts ++
genRecordToJson ptn pelts ++
(if null relts then [] else genRecordType ctx domainName rtn relts) ++
(if null relts then [] else genRecordFromJson rtn relts) ++
commandInstance
where
cn = commandName domainName commandElt
ptn = commandParamsNameHS domainName commandElt
rtn = commandNameHS domainName commandElt
pelts = D.commandParameters commandElt
relts = D.commandReturns commandElt
commandAssociatedType = if null relts then "()" else rtn
commandInstance =
["instance Command " <> ptn <> " where"] ++
indent ["type CommandResponse " <> ptn <> " = " <> commandAssociatedType] ++
indent ["commandName _ = \"" <> cn <> "\""] ++
indent ["fromJSON = const . A.Success . const ()" | (null relts)]
genRecordType :: Context -> T.Text -> T.Text -> [D.Property] -> [T.Text]
genRecordType ctx domainName recName props =
(do
p <- props
e <- maybeToList $ D.propertyEnum p
genTypeEnum ctx (tyNameHS "" (fieldNameHS recName $ D.propertyName p)) e) ++
[ "data " <> recName <> " = " <> recName] ++
(indent $ case props of
[] -> []
_ ->
["{"] ++
(indent $ do
(isLast, prop) <- markLast props
let (fn, fty) = propertyHsSig ctx domainName recName prop
comma = if isLast then "" else ","
formatDescription (D.propertyDescription prop) ++
[fn <> " :: " <> fty <> comma]) ++
["}"]) ++
[" deriving (Eq, Show)"]
genRecordSmartConstructor :: Context -> T.Text -> T.Text -> [D.Property] -> [T.Text]
genRecordSmartConstructor ctx domainName recName props =
[conName] ++
(indent $ do
(isFirstParam, (mbDesc, ty)) <- markFirst types
let d1 = formatDescription mbDesc
d2
| length d1 == 0 = d1
| otherwise = "{-" : d1 ++ ["-}"]
d2 ++
[(if isFirstParam then ":: " else "-> ") <> ty]) ++
[conName] ++
indent args ++
indent ["= " <> recName] ++
(indent . indent $ do
prop <- props
pure $ if D.propertyOptional prop
then "Nothing"
else argName prop)
where
conName = uncapitalizeFirst recName
requiredProps = filter (not . D.propertyOptional) props
types = (do
prop <- requiredProps
let (_, ty) = propertyHsSig ctx domainName recName prop
pure (D.propertyDescription prop, ty)) ++
[ (Nothing, recName) ]
args = do
prop <- requiredProps
guard . not $ D.propertyOptional prop
pure $ argName prop
argName = ("arg_" <>) . fieldNameHS recName . D.propertyName
genRecordFromJson :: T.Text -> [D.Property] -> [T.Text]
genRecordFromJson recName props =
["instance FromJSON " <> recName <> " where"] ++
(indent $ case props of
[] -> ["parseJSON _ = pure " <> recName]
_ ->
["parseJSON = A.withObject \"" <> recName <> "\" $ \\o -> " <> recName] ++
(indent $ do
(isFirst, prop) <- markFirst props
pure $
(if isFirst then "<$>" else "<*>") <>
" o " <> (if D.propertyOptional prop then "A..:?" else "A..:") <>
" \"" <> D.propertyName prop <> "\""))
genRecordToJson :: T.Text -> [D.Property] -> [T.Text]
genRecordToJson recName params =
["instance ToJSON " <> recName <> " where"] ++
(case params of
[] -> [" toJSON _ = A.Null"]
_ ->
[" toJSON p = A.object $ catMaybes ["] ++
[
" " <> T.intercalate ",\n "
[ "(\"" <> D.propertyName prop <> "\" A..=) <$> " <>
(if D.propertyOptional prop then "" else "Just ") <>
"(" <> fieldNameHS recName (D.propertyName prop) <> " p)"
| prop <- params
]
] ++
[" ]"])
propertyHsSig :: Context -> T.Text -> T.Text -> D.Property -> (T.Text, T.Text)
propertyHsSig ctx domainName recName prop = case D.propertyEnum prop of
Just _ -> (fn, (if D.propertyOptional prop then "Maybe " else "") <> ftn)
Nothing -> (fn, ) $ genEltType ctx domainName (D.propertyOptional prop)
(D.propertyType prop)
(D.propertyRef prop)
(D.propertyItems prop)
where
ftn = tyNameHS "" fn
fn = fieldNameHS recName . D.propertyName $ prop
genTypeSynonynm :: Context -> T.Text -> T.Text -> T.Text
genTypeSynonynm ctx domainName typeName = T.unwords ["type", typeName, "=", typeCDPToHS ctx domainName "object" Nothing]
----------- Generation rules -----------
----- Docs -----
formatComponentDescription :: Component -> T.Text
formatComponentDescription component = T.unlines $
["{- |"] ++
(do
(delt, _) <- Map.elems $ cDomDeps component
["= " <> domainName delt] <> [""] ++
maybe [] T.lines (D.domainDescription delt)) ++
["-}"]
formatDescription :: Maybe T.Text -> [T.Text]
formatDescription = maybe [] (indentWith "-- | " "-- " . T.lines)
----- Imports -----
importDomain :: Bool -> T.Text -> T.Text
importDomain as' domainName = T.unwords $
if as' then imp ++ ["as", domainName] else imp
where
imp = ["import", domainModuleName domainName]
componentToImport :: [ComponentName] -> [T.Text]
componentToImport = map (("module " <>) . domainModuleName . unComponentName)
protocolModuleHeader :: [ComponentName] -> T.Text
protocolModuleHeader names = T.unlines
[ mod
, "( " <> T.intercalate "\n, " exports
, ") where"
]
where
mod = T.unwords [ "module", protocolModuleName ]
exports = componentToImport names
protocolModuleName :: T.Text
protocolModuleName = "CDP.Domains"
domainModuleHeader :: T.Text -> T.Text
domainModuleHeader domainName = T.unwords ["module", domainModuleName domainName, "(module", domainModuleName domainName <>")", "where"]
domainModuleName :: T.Text -> T.Text
domainModuleName domainName = T.intercalate "." ["CDP", "Domains", domainName]
domainQualifiedName :: T.Text -> T.Text -> T.Text
domainQualifiedName domainName n = T.intercalate "." [domainName, n]
domainName :: D.Domain -> T.Text
domainName = D.domainDomain
----- JSON -----
genFromJSONInstanceEnum :: T.Text -> [T.Text] -> [T.Text] -> [T.Text]
genFromJSONInstanceEnum name vals hsVals =
["instance FromJSON " <> name <> " where"] ++
(indent $
["parseJSON = A.withText " <> (T.pack . show) name <> " $ \\v -> case v of"] ++
(indent $ do
(v, hsv) <- zip vals (map ("pure " <>) hsVals) ++ [emptyCase name]
pure $ T.pack (show v) <> " -> " <> hsv))
genToJSONInstanceEnum :: T.Text -> [T.Text] -> [T.Text] -> [T.Text]
genToJSONInstanceEnum name vals hsVals =
["instance ToJSON " <> name <> " where"] ++
(indent $
["toJSON v = A.String $ case v of"] ++
(indent $ do
(v, hsv) <- zip vals hsVals
pure $ hsv <> " -> " <> T.pack (show v)))
----- Types -----
genEltType :: Context -> T.Text -> Bool -> Maybe T.Text -> Maybe T.Text -> Maybe D.Items -> T.Text
genEltType ctx name isOptional t1 t2 items = (if isOptional then "Maybe " else "") <> (leftType ctx name t1 t2 items)
leftType :: Context -> T.Text -> Maybe T.Text -> Maybe T.Text -> Maybe D.Items -> T.Text
leftType ctx _ (Just ty1) (Just ty2) _ = error "impossible"
leftType ctx domain (Just ty) _ itemsElt = typeCDPToHS ctx domain ty itemsElt
leftType ctx domain _ (Just ty) itemsElt = typeCDPToHS ctx domain ty itemsElt
leftType ctx _ _ _ _ = error "no type found"
typeCDPToHS :: Context -> T.Text -> T.Text -> Maybe D.Items -> T.Text
typeCDPToHS ctx _ "object" _ = "[(T.Text, T.Text)]"
typeCDPToHS ctx domain "array" (Just items) = "[" <> (leftType ctx domain (D.itemsType items) (D.itemsRef items) Nothing) <> "]"
typeCDPToHS ctx _ ty (Just items) = error . T.unpack $ "non-array type with items: " <> ty
typeCDPToHS ctx domain ty Nothing = convertType ctx domain ty
convertType :: Context -> T.Text -> T.Text -> T.Text
convertType _ _ "string" = "T.Text"
convertType _ _ "integer" = "Int"
convertType _ _ "boolean" = "Bool"
convertType _ _ "number" = "Double"
convertType _ _ "()" = "()"
convertType _ _ "any" = "A.Value"
convertType _ _ "array" = error "got array conversion"
convertType _ _ "object" = error "got object type"
convertType _ _ "" = error "got empty type"
convertType ctx domain s = case T.splitOn "." s of
[otherDomain, ty] -> if cn otherDomain == cn domain
then tyNameHS otherDomain ty -- both domains are in the same component, so the type is not qualified
else domainQualifiedName (cn otherDomain) (tyNameHS otherDomain ty)
_ -> tyNameHS domain s
where
cn dn = unComponentName . domainToComponentName ctx $ dn
----- Dependencies -----
{- : Domain dependencies specified in the protocol seem inaccurate:
https://bugs.chromium.org/p/chromium/issues/detail?id=1354980#c0
So they are collected by inspecting each domain.
-}
domainToComponentName :: Context -> T.Text -> ComponentName
domainToComponentName ctx domainName = cName . (flip (Map.!) domainName) . ctxDomainComponents $ ctx
newtype ComponentName = ComponentName { unComponentName :: T.Text }
deriving (Show, Eq, Ord)
type DomainDependencies = Map.Map T.Text (D.Domain, [T.Text])
data Component = Component {
cName :: ComponentName
, cDomDeps :: DomainDependencies
}
type DomainComponents = Map.Map T.Text Component
-- original domain name, component the domain belongs to
-- domain name for each vertex is the modified domain name
type Vertex = (D.Domain, T.Text, [T.Text]) -- domain, domain name, domain dependencies
domainComponents :: [D.Domain] -> DomainComponents
domainComponents delts = Map.fromList . concatMap verticesToDomainComponents $ vsc2
where
vsc2 = map removeSameComponentDependencies vsc1
vsc1 = map Graph.flattenSCC . Graph.stronglyConnCompR $ g
g = map (\delt -> (delt, domainName delt, deps delt)) delts
deps = Set.toList . domainDependencies
verticesToDomainComponents :: [Vertex] -> [(T.Text, Component)]
verticesToDomainComponents vs = map (,c) dns
where
dns = map vertexToDomainName vs
c = verticesToComponent vs
vertexToDomainName :: Vertex -> T.Text
vertexToDomainName (_,dn,_) = dn
verticesToComponent :: [Vertex] -> Component
verticesToComponent vs = Component cn . Map.fromList . map toComponent $ vs
where
toComponent (delt,dn,deps) = (dn, (delt, deps))
cn = componentName vs
componentName :: [Vertex] -> ComponentName
componentName [v] = ComponentName $ vertexToDomainName v
componentName vs = ComponentName $ mconcat . map vertexToDomainName $ vs
removeSameComponentDependencies :: [Vertex] -> [Vertex]
removeSameComponentDependencies vs = map (go vs) vs
where
go vs (delt, dn, deps) = (delt,dn,) . Set.toList $
(Set.fromList deps) `Set.difference` dns
dns = Set.fromList $ map vertexToDomainName vs
domainDependencies :: D.Domain -> Set.Set T.Text
domainDependencies delt = removeSelf . Set.fromList . concat $
[ concatMap typeDependencies $ D.domainTypes delt
, concatMap commandDependencies $ D.domainCommands delt
, concatMap eventDependencies $ D.domainEvents delt
]
where
removeSelf = Set.filter (/= dn)
dn = domainName delt
typeDependencies :: D.Type -> [T.Text]
typeDependencies telt = case D.typeEnum telt of
Just _ -> []
Nothing -> case D.typeType telt of
"array" -> maybe [] pure . join $ itemDependencies <$> D.typeItems telt
"object" -> fromMaybe [] $ catMaybes . map propertyDependencies <$> D.typeProperties telt
s -> maybe [] pure $ refTypeToDomain s
commandDependencies :: D.Command -> [T.Text]
commandDependencies celt = concat
[ catMaybes . map propertyDependencies $ D.commandReturns celt
, catMaybes . map propertyDependencies $ D.commandParameters celt
]
eventDependencies :: D.Event -> [T.Text]
eventDependencies evelt =
catMaybes . map propertyDependencies $ D.eventParameters evelt
propertyDependencies :: D.Property -> Maybe T.Text
propertyDependencies pelt = case D.propertyEnum pelt of
Just _ -> Nothing
Nothing -> case D.propertyRef pelt of
Just r -> refTypeToDomain r
Nothing -> case D.propertyType pelt of
Just "object" -> Nothing
Just "array" -> itemDependencies =<< D.propertyItems pelt
Just s -> refTypeToDomain s
_ -> Nothing
itemDependencies :: D.Items -> Maybe T.Text
itemDependencies itelt = refTypeToDomain =<< D.itemsRef itelt
----- Names -----
eventName :: T.Text -> D.Event -> T.Text
eventName domainName ev = (domainName <>) . ("." <>) . D.eventName $ ev
eventNameHS :: T.Text -> D.Event -> T.Text
eventNameHS domainName ev = tyNameHS domainName (D.eventName ev)
commandName :: T.Text -> D.Command -> T.Text
commandName domainName c = (domainName <>) . ("." <>) . D.commandName $ c
commandNameHS :: T.Text -> D.Command -> T.Text
commandNameHS domainName c = tyNameHS domainName (D.commandName c)
commandParamsNameHS :: T.Text -> D.Command -> T.Text
commandParamsNameHS domainName c = paramsTypePrefix $ commandNameHS domainName c
typeNameHS :: T.Text -> D.Type -> T.Text
typeNameHS domainName t = tyNameHS domainName (D.typeId t)
tyNameHS :: T.Text -> T.Text -> T.Text
tyNameHS prefix tyName =
capitalizeFirst prefix <> capitalizeFirst (hyphensToCapitalize tyName)
fieldNameHS :: T.Text -> T.Text -> T.Text
fieldNameHS tyName fieldName = uncapitalizeFirst tyName <> capitalizeFirst fieldName
paramsTypePrefix :: T.Text -> T.Text
paramsTypePrefix = ("P" <>)
refTypeToDomain :: T.Text -> Maybe T.Text
refTypeToDomain r = case T.splitOn "." r of
[domain, _] -> Just domain
_ -> Nothing
-----
emptyCase :: T.Text -> (T.Text, T.Text)
emptyCase name = ("_", T.unwords ["fail", (T.pack . show) $ "failed to parse " <> name])
derivingBase :: T.Text
derivingBase = "deriving (Eq, Show, Read)"
derivingOrd :: T.Text
derivingOrd = "deriving (Ord, Eq, Show, Read)"
----------- Utilities -----------
indent :: [T.Text] -> [T.Text]
indent = indentWith " " " "
indentWith :: T.Text -> T.Text -> [T.Text] -> [T.Text]
indentWith _ _ [] = []
indentWith firstLine otherLines (x : xs) =
(firstLine <> x) : map (otherLines <>) xs
capitalizeFirst :: T.Text -> T.Text
capitalizeFirst t = maybe t (\(first, rest) -> T.singleton (toUpper first) `T.append` rest) . T.uncons $ t
uncapitalizeFirst :: T.Text -> T.Text
uncapitalizeFirst t = maybe t (\(first, rest) -> T.singleton (toLower first) `T.append` rest) . T.uncons $ t
markFirst :: [a] -> [(Bool, a)]
markFirst = zip (True : repeat False)
markLast :: [a] -> [(Bool, a)]
markLast [] = []
markLast (x : []) = [(True, x)]
markLast (x : y : z) = (False, x) : markLast (y : z)
-- | Changes things like "a-rate" to "aRate"
hyphensToCapitalize :: T.Text -> T.Text
hyphensToCapitalize txt
| T.null after = before
| otherwise =
before <> capitalizeFirst (hyphensToCapitalize (T.drop 1 after))
where
(before, after) = T.break (== '-') txt