{-# LANGUAGE TemplateHaskell, ScopedTypeVariables, OverloadedStrings #-} module Data.Aeson.AutoType.Format( displaySplitTypes, splitTypeByLabel, unificationCandidates, unifyCandidates ) where import Control.Arrow ((&&&)) import Control.Lens.TH import Control.Lens import Control.Monad (forM) import Control.Exception(assert) --import qualified Data.ByteString.Lazy.Char8 as BSL import qualified Data.HashMap.Strict as Map import qualified Data.Set as Set --import qualified Data.Vector as V --import Data.Aeson --import Data.Aeson.Types import qualified Data.Text as Text import Data.Text (Text) import Data.Set (Set ) import Data.List (foldl1') import Data.Char (isAlpha) import Control.Monad.State.Class import Control.Monad.State.Strict(State, runState) import qualified Data.Graph as Graph import Data.Aeson.AutoType.Type import Data.Aeson.AutoType.Extract import Data.Aeson.AutoType.Util () fst3 :: (t, t1, t2) -> t fst3 (a, _b, _c) = a data DeclState = DeclState { _decls :: [Text] , _counter :: Int } deriving (Eq, Show, Ord) makeLenses ''DeclState type DeclM = State DeclState type Map k v = Map.HashMap k v stepM :: DeclM Int stepM = counter %%= (\i -> (i, i+1)) tShow :: (Show a) => a -> Text tShow = Text.pack . show wrapDecl :: Text -> Text -> Text wrapDecl identifier contents = Text.unlines [header, contents, " } deriving (Show,Eq)" ,"\nderiveJSON defaultOptions ''" `Text.append` identifier] where header = Text.concat ["data ", identifier, " = ", identifier, " { "] -- | Makes a generic identifier name. genericIdentifier :: DeclM Text genericIdentifier = do i <- stepM return $! "Obj" `Text.append` tShow i -- * Naive type printing. newDecl :: Text -> [(Text, Type)] -> DeclM Text newDecl identifier kvs = do attrs <- forM kvs $ \(k, v) -> do formatted <- formatType v return (k, formatted) let decl = wrapDecl identifier $ fieldDecls attrs decls %%= (\ds -> ((), decl:ds)) return identifier where fieldDecls attrList = Text.intercalate ",\n" $ map fieldDecl attrList fieldDecl :: (Text, Text) -> Text fieldDecl (name, fType) = Text.concat [" ", normalizeFieldName identifier name, " :: ", fType] normalizeFieldName :: Text -> Text -> Text normalizeFieldName identifier = escapeKeywords . uncapitalize . (normalizeTypeName identifier `Text.append`) . normalizeTypeName keywords :: Set Text keywords = Set.fromList ["type", "data", "module"] escapeKeywords :: Text -> Text escapeKeywords k | k `Set.member` keywords = k `Text.append` "_" escapeKeywords k = k emptySetLikes :: Set Type emptySetLikes = Set.fromList [TNull, TArray $ TUnion $ Set.fromList []] formatType :: Type -> DeclM Text formatType TString = return "Text" formatType TNum = return "Int" formatType TBool = return "Bool" formatType (TLabel l) = return $ normalizeTypeName l formatType (TUnion u) | uu <- u `Set.difference` emptySetLikes, Set.size uu == 1 = do fmt <- formatType $ head $ Set.toList uu return $ "Maybe " `Text.append` fmt formatType (TUnion u) = do tys <- forM (Set.toList u) formatType return $ mkUnion tys where mkUnion [] = emptyTypeRepr mkUnion nonEmpty = foldr1 mkEither nonEmpty where mkEither a b = Text.concat ["Either (", a, ") (", b, ")"] formatType (TArray a) = do inner <- formatType a return $ Text.concat ["[", inner, "]"] formatType (TObj o) = do ident <- genericIdentifier newDecl ident d where d = Map.toList $ unDict o formatType e | e `Set.member` emptySetLikes = return emptyTypeRepr formatType t = return $ "ERROR: Don't know how to handle: " `Text.append` tShow t emptyTypeRepr :: Text emptyTypeRepr = "Maybe Text" -- default... runDecl :: DeclM a -> Text runDecl decl = Text.unlines $ finalState ^. decls where initialState = DeclState [] 1 (_, finalState) = runState decl initialState -- * Splitting object types by label for unification. type TypeTree = Map Text [Type] type TypeTreeM a = State TypeTree a addType :: Text -> Type -> TypeTreeM () addType label typ = modify $ Map.insertWith (++) label [typ] splitTypeByLabel' :: Text -> Type -> TypeTreeM Type splitTypeByLabel' _ TString = return TString splitTypeByLabel' _ TNum = return TNum splitTypeByLabel' _ TBool = return TBool splitTypeByLabel' _ TNull = return TNull splitTypeByLabel' _ (TLabel r) = assert False $ return $ TLabel r -- unnecessary? splitTypeByLabel' l (TUnion u) = do m <- mapM (splitTypeByLabel' l) $ Set.toList u return $! TUnion $! Set.fromList m splitTypeByLabel' l (TArray a) = do m <- splitTypeByLabel' (l `Text.append` "Elt") a return $! TArray m splitTypeByLabel' l (TObj o) = do kvs <- forM d $ \(k, v) -> do component <- splitTypeByLabel' k v return (k, component) addType l (TObj $ Dict $ Map.fromList kvs) return $! TLabel l where d = Map.toList $ unDict o --splitTypeByLabel' l t = error $ "ERROR: Don't know how to handle: " ++ show t splitTypeByLabel :: Text -> Type -> Map Text Type splitTypeByLabel topLabel t = Map.map (foldl1' unifyTypes) finalState where job = splitTypeByLabel' topLabel t -- addType topLabel r initialState = Map.empty (_, finalState) = runState job initialState formatObjectType :: Text -> Type -> DeclM Text formatObjectType identifier (TObj o) = newDecl identifier d where d = Map.toList $ unDict o formatObjectType _ other = formatType other displaySplitTypes :: Map Text Type -> Text displaySplitTypes dict = runDecl declarations where declarations = forM (toposort dict) $ \(name, typ) -> do let name' = normalizeTypeName name formatObjectType name' typ normalizeTypeName :: Text -> Text normalizeTypeName = escapeKeywords . Text.concat . map capitalize . filter (not . Text.null) . Text.split (not . isAlpha) capitalize :: Text -> Text capitalize word = Text.toUpper first `Text.append` rest where (first, rest) = Text.splitAt 1 word uncapitalize :: Text -> Text uncapitalize word = Text.toLower first `Text.append` rest where (first, rest) = Text.splitAt 1 word -- | Topological sorting of splitted types so that it is accepted declaration order. toposort :: Map Text Type -> [(Text, Type)] toposort splitted = map ((id &&& (splitted Map.!)) . fst3 . graphKey) $ Graph.topSort graph where (graph, graphKey) = Graph.graphFromEdges' $ map makeEntry $ Map.toList splitted makeEntry (k, v) = (k, k, allLabels v) -- | Computes all type labels referenced by a given type. allLabels :: Type -> [Text] allLabels = flip go [] where go (TLabel l) ls = l:ls go (TArray t) ls = go t ls go (TUnion u) ls = Set.foldr go ls u go (TObj o) ls = Map.foldr go ls $ unDict o go _other ls = ls -- * Finding candidates for extra unifications -- | For a given splitted types, it returns candidates for extra -- unifications. unificationCandidates :: Map.HashMap t Type -> [[t]] unificationCandidates = Map.elems . Map.filter candidates . Map.fromListWith (++) . map entry . Map.toList where candidates [ ] = False candidates [_] = False candidates _ = True entry (k, TObj o) = (Set.fromList $ Map.keys $ unDict o, [k]) entry (_, other ) = error $ "Unexpected type: " ++ show other -- | Unifies candidates on a give input list. unifyCandidates :: [[Text]] -> Map Text Type -> Map Text Type unifyCandidates candidates splitted = Map.map (remapLabels labelMapping) $ replacements splitted where unifiedType :: [Text] -> Type unifiedType cset = foldr1 unifyTypes $ map (splitted Map.!) cset replace :: [Text] -> Map Text Type -> Map Text Type replace cset@(c:_) s = Map.insert c (unifiedType cset) (foldr Map.delete s cset) replace [] _ = error "Empty cset in replace" replacements :: Map Text Type -> Map Text Type replacements s = foldr replace s candidates labelMapping :: Map Text Text labelMapping = Map.fromList $ concatMap mapEntry candidates mapEntry cset@(c:_) = [(x, c) | x <- cset] mapEntry [] = error "Empty cset in mapEntry" -- | Remaps type labels according to a `Map`. remapLabels :: Map Text Text -> Type -> Type remapLabels ls (TObj o) = TObj $ Dict $ Map.map (remapLabels ls) $ unDict o remapLabels ls (TArray t) = TArray $ remapLabels ls t remapLabels ls (TUnion u) = TUnion $ Set.map (remapLabels ls) u remapLabels ls (TLabel l) = TLabel $ Map.lookupDefault l l ls remapLabels _ other = other