packages feed

json-autotype-0.2.0.0: Data/Aeson/AutoType/Format.hs

{-# 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, 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 qualified Data.Text.IO               as Text
import           Data.Text                 (Text)
import           Data.Set                  (Set )
import           Data.List                 (sort, foldl1')
import           Data.Ord                  (Ord(..), comparing)
import           Data.Char                 (isAlpha)
--import           Data.Tuple.Utils          (fst3)
import           Control.Monad.State.Class
import           Control.Monad.State.Strict(State, runState)
import           Data.Hashable             (Hashable(..))
import qualified Data.Graph          as Graph

import           Data.Aeson.AutoType.Type
import           Data.Aeson.AutoType.Extract
import           Data.Aeson.AutoType.Util

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 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 = 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 identifier = escapeKeywords             .
                                uncapitalize               .
                                (normalizeTypeName identifier `Text.append`) .
                                normalizeTypeName

keywords = Set.fromList ["type", "data", "module"]

escapeKeywords k | k `Set.member` keywords = k `Text.append` "_"
escapeKeywords k                           = k

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 = "Maybe Text" -- default...

formatType = runDecl . formatType'

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' l  TString   = return TString
splitTypeByLabel' l  TNum      = return TNum
splitTypeByLabel' l  TBool     = return TBool
splitTypeByLabel' l  TNull     = return TNull
splitTypeByLabel' l (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 identifier (TObj o) = newDecl identifier d
  where
    d = Map.toList $ unDict o
formatObjectType identifier other    = formatType' other

displaySplitTypes dict = runDecl decls
  where
    decls =
      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.elems             .
                        Map.filter candidates .
                        Map.fromListWith (++) .
                        map entry             .
                        Map.toList
  where
    candidates [ ] = False
    candidates [a] = 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)
    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]

-- | 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 ls other      = other