packages feed

json-autotype 1.0.18 → 1.1.0

raw patch · 19 files changed

+1111/−568 lines, 19 filesdep ~optparse-applicativePVP ok

version bump matches the API change (PVP)

Dependency ranges changed: optparse-applicative

API changes (from Hackage documentation)

- Data.Aeson.AutoType.Alternative: data (:|:) a b
- Data.Aeson.AutoType.CodeGen: writeHaskellModule :: FilePath -> HashMap Text Type -> IO ()
- Data.Aeson.AutoType.Format: displaySplitTypes :: Map Text Type -> Text
- Data.Aeson.AutoType.Format: instance GHC.Classes.Eq Data.Aeson.AutoType.Format.DeclState
- Data.Aeson.AutoType.Format: instance GHC.Classes.Ord Data.Aeson.AutoType.Format.DeclState
- Data.Aeson.AutoType.Format: instance GHC.Generics.Generic Data.Aeson.AutoType.Format.DeclState
- Data.Aeson.AutoType.Format: instance GHC.Show.Show Data.Aeson.AutoType.Format.DeclState
- Data.Aeson.AutoType.Format: normalizeTypeName :: Text -> Text
- Data.Aeson.AutoType.Format: splitTypeByLabel :: Text -> Type -> Map Text Type
- Data.Aeson.AutoType.Format: unificationCandidates :: HashMap t Type -> [[t]]
- Data.Aeson.AutoType.Format: unifyCandidates :: [[Text]] -> Map Text Type -> Map Text Type
- Data.Aeson.AutoType.Pretty: instance Text.PrettyPrint.GenericPretty.Out a => Text.PrettyPrint.GenericPretty.Out (Data.Set.Base.Set a)
+ Data.Aeson.AutoType.Alternative: data a (:|:) b
+ Data.Aeson.AutoType.CodeGen: Elm :: Lang
+ Data.Aeson.AutoType.CodeGen: Haskell :: Lang
+ Data.Aeson.AutoType.CodeGen: data Lang
+ Data.Aeson.AutoType.CodeGen: runModule :: Lang -> [String] -> IO ExitCode
+ Data.Aeson.AutoType.CodeGen: writeModule :: Lang -> FilePath -> Text -> HashMap Text Type -> IO ()
+ Data.Aeson.AutoType.Format: capitalize :: Text -> Text
+ Data.Aeson.AutoType.Format: uncapitalize :: Text -> Text
+ Data.Aeson.AutoType.Pretty: instance Text.PrettyPrint.GenericPretty.Out a => Text.PrettyPrint.GenericPretty.Out (Data.Set.Internal.Set a)
- Data.Aeson.AutoType.CodeGen: defaultOutputFilename :: FilePath
+ Data.Aeson.AutoType.CodeGen: defaultOutputFilename :: Lang -> FilePath

Files

CommonCLI.hs view
@@ -1,4 +1,4 @@-module CommonCLI(TypeOpts(..), unflag, tyOptParser, runghc) where+module CommonCLI(TypeOpts(..), unflag, tyOptParser) where  import           Data.Monoid                    ((<>)) import           Options.Applicative@@ -8,6 +8,7 @@  data TypeOpts = TyOptions {                   autounify :: Bool+                , toplevel  :: String                 , debug     :: Bool                 , test      :: Bool                 , suggest   :: Bool@@ -19,16 +20,10 @@ tyOptParser :: Parser TypeOpts tyOptParser  = TyOptions             <$> unflag (long "no-autounify" <> help "Do not automatically unify suggested candidates")+            <*> strOption (short 't'        <>+                           long "toplevel"  <> value "TopLevel"+                                            <> help "Name for toplevel data type")             <*> switch (long "debug"        <> help "Set this flag to see more debugging info"       )             <*> unflag (long "no-test"      <> help "Do not run generated parser afterwards"         )             <*> unflag (long "no-suggest"   <> help "Do not suggest candidates for unification"      )--runghc :: [String] -> IO ExitCode-runghc arguments = do-    maybeStack <- System.Environment.lookupEnv "STACK_EXEC"-    maybeCabal <- System.Environment.lookupEnv "CABAL_SANDBOX_CONFIG"-    let execPrefix | Just stackExec   <- maybeStack = [stackExec, "exec", "--"]-                   | Just cabalConfig <- maybeCabal = ["cabal",   "exec", "--"]-                   | otherwise                      = []-    system (unwords $ execPrefix ++ ["runghc"] ++ arguments) 
Data/Aeson/AutoType/CodeGen.hs view
@@ -1,100 +1,35 @@ {-# LANGUAGE CPP               #-} {-# LANGUAGE OverloadedStrings #-}--- | Wrappers for generating prologue and epilogue code in Haskell.+-- | Code generation and test running in different languages. (Switchbox.) module Data.Aeson.AutoType.CodeGen(-    writeHaskellModule+    Lang(..)+  , writeModule+  , runModule   , defaultOutputFilename   ) where -import qualified Data.Text           as Text-import qualified Data.Text.IO        as Text-import           Data.Text+import           Data.Text(Text) import qualified Data.HashMap.Strict as Map-import           Control.Arrow               (first)-import           System.FilePath-import           System.IO- import           Data.Aeson.AutoType.Type-import           Data.Aeson.AutoType.Format-import           Data.Aeson.AutoType.Util --- | Default output filname is used, when there is no explicit output file path, or it is "-" (stdout).--- Default module name is consistent with it.-defaultOutputFilename :: FilePath-defaultOutputFilename = "JSONTypes.hs"--capitalize :: Text -> Text-capitalize input = Text.toUpper (Text.take 1 input)-                   `Text.append` Text.drop 1 input+import           Data.Aeson.AutoType.CodeGen.Haskell+import           Data.Aeson.AutoType.CodeGen.Elm -header :: Text -> Text-header moduleName = Text.unlines [-   "{-# LANGUAGE TemplateHaskell     #-}"-  ,"{-# LANGUAGE ScopedTypeVariables #-}"-  ,"{-# LANGUAGE RecordWildCards     #-}"-  ,"{-# LANGUAGE OverloadedStrings   #-}"-  ,"{-# LANGUAGE TypeOperators       #-}"-  ,"{-# LANGUAGE DeriveGeneric       #-}"-  ,""-  ,Text.concat ["module ", capitalize moduleName, " where"]-  ,""-  ,"import           System.Exit        (exitFailure, exitSuccess)"-  ,"import           System.IO          (stderr, hPutStrLn)"-  ,"import qualified Data.ByteString.Lazy.Char8 as BSL"-  ,"import           System.Environment (getArgs)"-  ,"import           Control.Monad      (forM_, mzero, join)"-  ,"import           Control.Applicative"-  ,"import           Data.Aeson.AutoType.Alternative"-  ,"import           Data.Aeson(decode, Value(..), FromJSON(..), ToJSON(..),"-#if MIN_VERSION_aeson(0,11,0)-  ,"                            pairs,"-#endif-  ,"                            (.:), (.:?), (.=), object)"-  ,"import           Data.Monoid"-  ,"import           Data.Text (Text)"-  ,"import qualified GHC.Generics" -  ,""-  ,"-- | Workaround for https://github.com/bos/aeson/issues/287."-  ,"o .:?? val = fmap join (o .:? val)"-  ,""]+-- | Available output languages.+data Lang = Haskell+          | Elm -epilogue :: Text-epilogue          = Text.unlines-  [""-  ,"parse :: FilePath -> IO TopLevel"-  ,"parse filename = do input <- BSL.readFile filename"-  ,"                    case decode input of"-  ,"                      Nothing -> fatal $ case (decode input :: Maybe Value) of"-  ,"                                           Nothing -> \"Invalid JSON file: \"     ++ filename"-  ,"                                           Just v  -> \"Mismatched JSON value from file: \" ++ filename"-  ,"                      Just r  -> return (r :: TopLevel)"-  ,"  where"-  ,"    fatal :: String -> IO a"-  ,"    fatal msg = do hPutStrLn stderr msg"-  ,"                   exitFailure"-  ,""-  ,"main :: IO ()"-  ,"main = do"-  ,"  filenames <- getArgs"-  ,"  forM_ filenames (\\f -> parse f >>= (\\p -> p `seq` putStrLn $ \"Successfully parsed \" ++ f))"-  ,"  exitSuccess"-  ,""]+-- | Default output filname is used, when there is no explicit output file path, or it is "-" (stdout).+-- Default module name is consistent with it.+defaultOutputFilename :: Lang -> FilePath+defaultOutputFilename Haskell = defaultHaskellFilename+defaultOutputFilename Elm     = defaultElmFilename  -- | Write a Haskell module to an output file, or stdout if `-` filename is given.-writeHaskellModule :: FilePath -> Map.HashMap Text Type -> IO ()-writeHaskellModule outputFilename types =-    withFileOrHandle outputFilename WriteMode stdout $ \hOut -> do-      assertM (extension == ".hs")-      Text.hPutStrLn hOut $ header $ Text.pack moduleName-      -- We write types as Haskell type declarations to output handle-      Text.hPutStrLn hOut $ displaySplitTypes types-      Text.hPutStrLn hOut   epilogue-  where-    (moduleName, extension) =-       first normalizeTypeName'     $-       splitExtension               $-       if     outputFilename == "-"-         then defaultOutputFilename-         else outputFilename-    normalizeTypeName' = Text.unpack . normalizeTypeName . Text.pack+writeModule :: Lang -> FilePath -> Text -> Map.HashMap Text Type -> IO ()+writeModule Haskell = writeHaskellModule+writeModule Elm     = writeElmModule +-- | Run module in a given language.+runModule Haskell = runHaskellModule+runModule Elm     = runElmModule
+ Data/Aeson/AutoType/CodeGen/Elm.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Wrappers for generating prologue and epilogue code in Haskell.+module Data.Aeson.AutoType.CodeGen.Elm(+    defaultElmFilename+  , writeElmModule+  , runElmModule+  ) where++import qualified Data.Text           as Text+import qualified Data.Text.IO        as Text+import           Data.Text+import qualified Data.HashMap.Strict as Map+import           Control.Arrow               (first)+import           Control.Exception (assert)+import           Data.Monoid                 ((<>))+import           System.FilePath+import           System.IO++import           Data.Aeson.AutoType.Format+import           Data.Aeson.AutoType.Type+import           Data.Aeson.AutoType.Util+import           Data.Aeson.AutoType.CodeGen.ElmFormat++import Debug.Trace(trace)++defaultElmFilename = "JSONTypes.elm"++header :: Text -> Text+header moduleName = Text.unlines [+   Text.unwords ["module ", capitalize moduleName, " exposing(..)"]+  ,""+  ,"-- elm-package install toastal/either"+  ,"-- elm-package install NoRedInk/elm-decode-pipeline"+  ,"import Either               exposing (Either, mapBoth)"+  ,"import Json.Encode          exposing (..)"+  ,"import Json.Decode          exposing (..)"+  ,"import Json.Decode.Pipeline exposing (..)"+  ,""]++epilogue :: Text -> Text+epilogue toplevelName = Text.unlines []++-- | Write a Haskell module to an output file, or stdout if `-` filename is given.+writeElmModule :: FilePath -> Text -> Map.HashMap Text Type -> IO ()+writeElmModule outputFilename toplevelName types =+    withFileOrHandle outputFilename WriteMode stdout $ \hOut ->+      assert (trace extension extension == ".elm") $ do+        Text.hPutStrLn hOut $ header $ Text.pack moduleName+        -- We write types as Haskell type declarations to output handle+        Text.hPutStrLn hOut $ displaySplitTypes types+        Text.hPutStrLn hOut $ epilogue toplevelName+  where+    (moduleName, extension) =+       first normalizeTypeName'     $+       splitExtension               $+       if     outputFilename == "-"+         then defaultElmFilename+         else outputFilename+    normalizeTypeName' = Text.unpack . normalizeTypeName . Text.pack++runElmModule = error "Yet undefined!"
+ Data/Aeson/AutoType/CodeGen/ElmFormat.hs view
@@ -0,0 +1,332 @@+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE TemplateHaskell     #-}+{-# LANGuaGE ScopedTypeVariables #-}+{-# LANGUAGE ViewPatterns        #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGuaGE DeriveGeneric       #-}+{-# LANGuaGE FlexibleContexts    #-}+-- | Formatting type declarations and class instances for inferred types.+module Data.Aeson.AutoType.CodeGen.ElmFormat(+  displaySplitTypes,+  normalizeTypeName) where++import           Control.Arrow             ((&&&))+import           Control.Applicative       ((<$>), (<*>))+import           Control.Lens.TH+import           Control.Lens+import           Control.Monad             (forM)+import           Control.Exception(assert)+import qualified Data.HashMap.Strict        as Map+import           Data.Monoid+import qualified Data.Set                   as Set+import qualified Data.Text                  as Text+import           Data.Text                 (Text)+import           Data.Set                  (Set )+import           Data.List                 (foldl1')+import           Data.Char                 (isAlpha, isDigit)+import           Control.Monad.State.Class+import           Control.Monad.State.Strict(State, runState)+import qualified Data.Graph          as Graph+import           GHC.Generics              (Generic)++import           Data.Aeson.AutoType.Type+import           Data.Aeson.AutoType.Extract+import           Data.Aeson.AutoType.Split+import           Data.Aeson.AutoType.Format+import           Data.Aeson.AutoType.Util  ()++--import           Debug.Trace -- DEBUG+trace _ x = x++fst3 ::  (t, t1, t2) -> t+fst3 (a, _b, _c) = a++data DeclState = DeclState { _decls   :: [Text]+                           , _counter :: Int+                           }+  deriving (Eq, Show, Ord, Generic)++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++-- | Wrap a type alias.+wrapAlias :: Text -> Text -> Text+wrapAlias identifier contents = Text.unwords ["type alias ", identifier, "=", contents]++-- | Wrap a data type declaration+wrapDecl ::  Text -> Text -> Text+wrapDecl identifier contents = Text.unlines [header, contents, "  }"]+                                            --,"\nderiveJSON defaultOptions ''" `Text.append` identifier]+  where+    header = Text.concat ["type alias ", identifier, " = ", " { "]++-- | Explanatory type alias for making declarations+-- First element of the triple is original JSON identifier,+-- second element of the triple is the mapped identifier name in Haskell.+-- third element of the triple shows the type in a formatted way+type MappedKey = (Text, Text, Text, Type, Bool)++-- | Make Decoder declaration, given identifier (object name in Haskell) and mapping of its keys+-- from JSON to Haskell identifiers *in the same order* as in *data type declaration*.+makeDecoder ::  Text -> [MappedKey] -> Text+makeDecoder identifier contents =+  Text.unlines [+      Text.concat  [decodeIdentifier, " : Json.Decode.Decoder ", identifier]+    , Text.concat  [decodeIdentifier, " ="]+    , Text.unwords ["    Json.Decode.Pipeline.decode", identifier]+    , Text.unlines (makeParser identifier <$> contents) ]+  where+    decodeIdentifier         = decoderIdent identifier+    makeParser identifier (jsonId, _, _, ty, isOptional) = Text.unwords [+          "  |>"+        , if isOptional+             then "Json.Decode.Pipeline.optional"+             else "Json.Decode.Pipeline.required"+        , Text.concat ["\"", jsonId, "\""]+        , "(" <> getDecoder ty <> ")"] -- quote++getDecoder  TString    = "Json.Decode.string"+getDecoder  TNum       = "Json.Decode.float"+getDecoder  TBool      = "Json.Decode.bool"+getDecoder (TArray  t) = "Json.Decode.list (" <> getDecoder t <> ")"+getDecoder (TLabel  l) = decoderIdent l+getDecoder (TObj    o) = error "getDecoder cannot handle complex object types!"+getDecoder (TUnion  u) = error $ "getDecoder cannot yet handle union types:" <> show u++decoderIdent ident = "decode" <> capitalize (normalizeTypeName ident)+-- Contents example for wrapFromJSON:+-- " <$>+--"                           v .: "hexValue"  <*>+--"                           v .: "colorName\""++encoderIdent ident = "encode" <> capitalize (normalizeTypeName ident)++-- | Make Encoder declaration, given identifier (object name in Haskell) and mapping of its keys+-- from JSON to Haskell identifiers in the same order as in declaration+makeEncoder :: Text -> [MappedKey] -> Text+makeEncoder identifier contents =+    Text.unlines [+        Text.unwords [encoderIdent identifier, ":", identifier, "->", "Json.Encode.Value"]+      , encoderIdent identifier <> " record ="+      , "    Json.Encode.object ["+      , "        " <> (joinWith "\n      , " (makeEncoder <$> contents))+      , "    ]"+      ]+  where+    makeEncoder (jsonId, haskellId, _typeText, ty, _nullable) = Text.concat [+            "(", tShow jsonId, ", ", getEncoder ty, " <| record.", normalizeFieldName identifier jsonId, ")"+        ]+    --"answers",  Json.Encode.list <| List.map encodeAnswer <| record.answers+    escapeText = Text.pack . show . Text.unpack++getEncoder :: Type -> Text+getEncoder  TString   = "Json.Encode.string"+getEncoder  TNum      = "Json.Encode.float"+getEncoder  TBool     = "Json.Encode.bool"+getEncoder  TNull     = "Json.Encode.complexType"+getEncoder (TLabel l) = encoderIdent l+getEncoder (TArray e) = "Json.Encode.list <| List.map (" <> getEncoder e <> ")"+getEncoder (TObj   o) = error $ "Seeing direct object encoder: "         <> show o+getEncoder (TUnion u) = "oneOf [" <> joinWith ", " (makeUnionDecoder u) <> "]"++makeUnionDecoder u = error $ "Unfinished union decoding:" <> show u+-- Contents example for wrapToJSON+--"hexValue"  .= hexValue+--                                        ,"colorName" .= colorName]+-- | Join text with other as separator.+joinWith :: Text -> [Text] -> Text+joinWith _      []            = ""+joinWith joiner (aFirst:rest) = aFirst <> Text.concat (map (joiner <>) rest)++-- | Makes a generic identifier name.+genericIdentifier :: DeclM Text+genericIdentifier = do+  i <- stepM+  return $! "Obj" `Text.append` tShow i++-- * Printing a single data type declaration+newDecl :: Text -> [(Text, Type)] -> DeclM Text+newDecl identifier kvs = do attrs <- forM kvs $ \(k, v) -> do+                              formatted <- formatType v+                              return (k, normalizeFieldName identifier k, formatted, v, isNullable v)+                            let decl = Text.unlines [wrapDecl    identifier $ fieldDecls attrs+                                                    ,""+                                                    ,makeDecoder identifier              attrs+                                                    ,""+                                                    ,makeEncoder identifier              attrs]+                            addDecl decl+                            return identifier+  where+    fieldDecls attrList = Text.intercalate ",\n" $ map fieldDecl attrList+    fieldDecl :: (Text, Text, Text, Type, Bool) -> Text+    fieldDecl (_jsonName, haskellName, fType, _type, _nullable) = Text.concat [+                                                                    "    ", haskellName, " : ", fType]++addDecl decl = decls %%= (\ds -> ((), decl:ds))++-- | Add new type alias for Array type+newAlias :: Text -> Type -> DeclM Text+newAlias identifier content = do formatted <- formatType content+                                 addDecl $ Text.unlines [wrapAlias identifier formatted]+                                 return identifier++-- | Convert a JSON key name given by second argument,+-- from within a dictionary keyed with first argument,+-- into a name of Haskell record field (hopefully distinct from other such selectors.)+normalizeFieldName ::  Text -> Text -> Text+normalizeFieldName identifier = escapeKeywords             .+                                uncapitalize               .+                                (normalizeTypeName identifier `Text.append`) .+                                normalizeTypeName++keywords ::  Set Text+keywords = Set.fromList ["type", "alias", "exposing", "module", "class",+                         "where", "let", "do"]++escapeKeywords ::  Text -> Text+escapeKeywords k | k `Set.member` keywords = k `Text.append` "_"+escapeKeywords k                           = k++nonNullComponents = Set.toList . Set.filter (TNull /=)+-- | Format the type within DeclM monad, that records+-- the separate declarations on which this one is dependent.+formatType :: Type -> DeclM Text+formatType  TString                          = return "String"+formatType  TNum                             = return "Float"+formatType  TBool                            = return "Bool"+formatType (TLabel l)                        = return $ normalizeTypeName l+formatType (TUnion u)                        = wrap <$> case length nonNull of+                                                          0 -> return emptyTypeRepr+                                                          1 -> formatType $ head nonNull+                                                          _ -> alts <$> mapM formatType nonNull+  where+    nonNull = nonNullComponents u+    wrap                                :: Text -> Text+    wrap   inner  | TNull `Set.member` u = Text.concat ["(Maybe (", inner, "))"]+                  | otherwise            =                          inner+    alts :: [Text] -> Text+    alts [alt]        =      alt+    alts (alt:others) = join alt $ alts others+      where+        join fAlt fOthers = Text.concat ["Either (", fAlt, ") (", fOthers, ")"]+formatType (TArray a)                        = do inner <- formatType a+                                                  return $ Text.concat ["List (", 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 ComplexType)" -- default, accepts future extension where we found no data++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 (Map.toList $ unDict o) $ \(k, v) -> do+                                       component <- splitTypeByLabel' k v+                                       return (k, component)+                                    addType l (TObj $ Dict $ Map.fromList kvs)+                                    return $! TLabel l++-- | Splits initial type with a given label, into a mapping of object type names and object type structures.+splitTypeByLabel :: Text -> Type -> Map Text Type+splitTypeByLabel topLabel t = Map.map (foldl1' unifyTypes) finalState+  where+    finalize (TLabel l) = assert (l == topLabel) $ return ()+    finalize  topLevel  = addType topLabel topLevel+    initialState    = Map.empty+    (_, finalState) = runState (splitTypeByLabel' topLabel t >>= finalize) initialState+ -}+formatObjectType ::  Text -> Type -> DeclM Text+formatObjectType identifier (TObj o) = newDecl  identifier d+  where+    d = Map.toList $ unDict o+formatObjectType identifier  other   = newAlias identifier other++-- | Display an environment of types split by name.+displaySplitTypes ::  Map Text Type -> Text+displaySplitTypes dict = trace ("displaySplitTypes: " ++ show (toposort dict)) $ runDecl declarations+  where+    declarations =+      forM (toposort dict) $ \(name, typ) ->+        formatObjectType (normalizeTypeName name) typ++-- | Normalize type name by:+-- 1. Treating all characters that are not acceptable in Haskell variable name as end of word.+-- 2. Capitalizing each word, but a first (camelCase).+-- 3. Adding underscore if first character is non-alphabetic.+-- 4. Escaping Haskell keywords if the whole identifier is such keyword.+-- 5. If identifier is empty, then substituting "JsonEmptyKey" for its name.+normalizeTypeName :: Text -> Text+normalizeTypeName s  = ifEmpty "JsonEmptyKey"                  .+                       escapeKeywords                          .+                       escapeFirstNonAlpha                     .+                       Text.concat                             .+                       map capitalize                          .+                       filter     (not . Text.null)            .+                       Text.split (not . acceptableInVariable) $ s+  where+    ifEmpty x ""       = x+    ifEmpty _ nonEmpty = nonEmpty+    acceptableInVariable c = isAlpha c || isDigit c+    escapeFirstNonAlpha cs                  | Text.null cs =                   cs+    escapeFirstNonAlpha cs@(Text.head -> c) | isAlpha   c  =                   cs+    escapeFirstNonAlpha cs                                 = "_" `Text.append` cs++-- | 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++-- | 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
+ Data/Aeson/AutoType/CodeGen/Haskell.hs view
@@ -0,0 +1,111 @@+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}+-- | Wrappers for generating prologue and epilogue code in Haskell.+module Data.Aeson.AutoType.CodeGen.Haskell(+    writeHaskellModule+  , runHaskellModule+  , defaultHaskellFilename+  ) where++import qualified Data.Text           as Text+import qualified Data.Text.IO        as Text+import           Data.Text hiding (unwords)+import qualified Data.HashMap.Strict as Map+import           Control.Arrow               (first)+import           Control.Exception (assert)+import           Data.Monoid                 ((<>))+import           System.FilePath+import           System.IO+import           System.Process                 (system)+import qualified System.Environment             (lookupEnv)+import           System.Exit                    (ExitCode)++import           Data.Aeson.AutoType.Format+import           Data.Aeson.AutoType.Type+import           Data.Aeson.AutoType.CodeGen.HaskellFormat+import           Data.Aeson.AutoType.Util++-- | Default output filname is used, when there is no explicit output file path, or it is "-" (stdout).+-- Default module name is consistent with it.+defaultHaskellFilename :: FilePath+defaultHaskellFilename = "JSONTypes.hs"++header :: Text -> Text+header moduleName = Text.unlines [+   "{-# LANGUAGE TemplateHaskell     #-}"+  ,"{-# LANGUAGE ScopedTypeVariables #-}"+  ,"{-# LANGUAGE RecordWildCards     #-}"+  ,"{-# LANGUAGE OverloadedStrings   #-}"+  ,"{-# LANGUAGE TypeOperators       #-}"+  ,"{-# LANGUAGE DeriveGeneric       #-}"+  ,""+  ,Text.concat ["module ", capitalize moduleName, " where"]+  ,""+  ,"import           System.Exit        (exitFailure, exitSuccess)"+  ,"import           System.IO          (stderr, hPutStrLn)"+  ,"import qualified Data.ByteString.Lazy.Char8 as BSL"+  ,"import           System.Environment (getArgs)"+  ,"import           Control.Monad      (forM_, mzero, join)"+  ,"import           Control.Applicative"+  ,"import           Data.Aeson.AutoType.Alternative"+  ,"import           Data.Aeson(decode, Value(..), FromJSON(..), ToJSON(..),"+#if MIN_VERSION_aeson(0,11,0)+  ,"                            pairs,"+#endif+  ,"                            (.:), (.:?), (.=), object)"+  ,"import           Data.Monoid"+  ,"import           Data.Text (Text)"+  ,"import qualified GHC.Generics" +  ,""+  ,"-- | Workaround for https://github.com/bos/aeson/issues/287."+  ,"o .:?? val = fmap join (o .:? val)"+  ,""]++epilogue :: Text -> Text+epilogue toplevelName = Text.unlines+  [""+  ,"parse :: FilePath -> IO " <> toplevelName+  ,"parse filename = do input <- BSL.readFile filename"+  ,"                    case decode input of"+  ,"                      Nothing -> fatal $ case (decode input :: Maybe Value) of"+  ,"                                           Nothing -> \"Invalid JSON file: \"     ++ filename"+  ,"                                           Just v  -> \"Mismatched JSON value from file: \" ++ filename"+  ,"                      Just r  -> return (r :: " <> toplevelName <> ")"+  ,"  where"+  ,"    fatal :: String -> IO a"+  ,"    fatal msg = do hPutStrLn stderr msg"+  ,"                   exitFailure"+  ,""+  ,"main :: IO ()"+  ,"main = do"+  ,"  filenames <- getArgs"+  ,"  forM_ filenames (\\f -> parse f >>= (\\p -> p `seq` putStrLn $ \"Successfully parsed \" ++ f))"+  ,"  exitSuccess"+  ,""]++-- | Write a Haskell module to an output file, or stdout if `-` filename is given.+writeHaskellModule :: FilePath -> Text -> Map.HashMap Text Type -> IO ()+writeHaskellModule outputFilename toplevelName types =+    withFileOrHandle outputFilename WriteMode stdout $ \hOut ->+      assert (extension == ".hs") $ do+        Text.hPutStrLn hOut $ header $ Text.pack moduleName+        -- We write types as Haskell type declarations to output handle+        Text.hPutStrLn hOut $ displaySplitTypes types+        Text.hPutStrLn hOut $ epilogue toplevelName+  where+    (moduleName, extension) =+       first normalizeTypeName'     $+       splitExtension               $+       if     outputFilename == "-"+         then defaultHaskellFilename+         else outputFilename+    normalizeTypeName' = Text.unpack . normalizeTypeName . Text.pack++runHaskellModule :: [String] -> IO ExitCode+runHaskellModule arguments = do+    maybeStack <- System.Environment.lookupEnv "STACK_EXEC"+    maybeCabal <- System.Environment.lookupEnv "CABAL_SANDBOX_CONFIG"+    let execPrefix | Just stackExec <- maybeStack = [stackExec, "exec", "--"]+                   | Just _         <- maybeCabal = ["cabal",   "exec", "--"]+                   | otherwise                    = []+    system (Prelude.unwords $ execPrefix ++ ["runghc"] ++ arguments)
+ Data/Aeson/AutoType/CodeGen/HaskellFormat.hs view
@@ -0,0 +1,290 @@+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE TemplateHaskell     #-}+{-# LANGuaGE ScopedTypeVariables #-}+{-# LANGUAGE ViewPatterns        #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGuaGE DeriveGeneric       #-}+{-# LANGuaGE FlexibleContexts    #-}+-- | Formatting type declarations and class instances for inferred types. +module Data.Aeson.AutoType.CodeGen.HaskellFormat(+  displaySplitTypes, normalizeTypeName+) where++import           Control.Arrow             ((&&&))+import           Control.Applicative       ((<$>), (<*>))+import           Control.Lens.TH+import           Control.Lens+import           Control.Monad             (forM)+import           Control.Exception(assert)+import qualified Data.HashMap.Strict        as Map+import           Data.Monoid+import qualified Data.Set                   as Set+import qualified Data.Text                  as Text+import           Data.Text                 (Text)+import           Data.Set                  (Set )+import           Data.List                 (foldl1')+import           Data.Char                 (isAlpha, isDigit)+import           Control.Monad.State.Class+import           Control.Monad.State.Strict(State, runState)+import qualified Data.Graph          as Graph+import           GHC.Generics              (Generic)++import           Data.Aeson.AutoType.Type+import           Data.Aeson.AutoType.Extract+import           Data.Aeson.AutoType.Format+import           Data.Aeson.AutoType.Util  ()++--import           Debug.Trace -- DEBUG+trace _ x = x++fst3 ::  (t, t1, t2) -> t+fst3 (a, _b, _c) = a++data DeclState = DeclState { _decls   :: [Text]+                           , _counter :: Int+                           }+  deriving (Eq, Show, Ord, Generic)++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 ++-- | Wrap a type alias.+wrapAlias :: Text -> Text -> Text+wrapAlias identifier contents = Text.unwords ["type", identifier, "=", contents]++-- | Wrap a data type declaration+wrapDecl ::  Text -> Text -> Text+wrapDecl identifier contents = Text.unlines [header, contents, "  } deriving (Show,Eq,GHC.Generics.Generic)"]+                                            --,"\nderiveJSON defaultOptions ''" `Text.append` identifier]+  where+    header = Text.concat ["data ", identifier, " = ", identifier, " { "]++-- | Explanatory type alias for making declarations+-- First element of the triple is original JSON identifier,+-- second element of the triple is the mapped identifier name in Haskell.+-- third element of the triple shows the type in a formatted way+type MappedKey = (Text, Text, Text, Bool)++-- | Make ToJSON declaration, given identifier (object name in Haskell) and mapping of its keys+-- from JSON to Haskell identifiers *in the same order* as in *data type declaration*.+makeFromJSON ::  Text -> [MappedKey] -> Text+makeFromJSON identifier contents =+  Text.unlines [+      Text.unwords ["instance FromJSON", identifier, "where"]+    , Text.unwords ["  parseJSON (Object v) =", makeParser identifier contents]+    , "  parseJSON _          = mzero" ]+  where+    makeParser identifier [] = Text.unwords ["return ", identifier]+    makeParser identifier _  = Text.unwords [identifier, "<$>", inner]+    inner                    = " <*> " `Text.intercalate`+                                  map takeValue contents+    takeValue (jsonId, _, ty, True ) = Text.concat ["v .:?? \"", jsonId, "\""] -- nullable types+    takeValue (jsonId, _, _ , False) = Text.concat ["v .:   \"", jsonId, "\""]+-- Contents example for wrapFromJSON:+-- " <$>+--"                           v .: "hexValue"  <*>+--"                           v .: "colorName\""++-- | Make ToJSON declaration, given identifier (object name in Haskell) and mapping of its keys+-- from JSON to Haskell identifiers in the same order as in declaration+makeToJSON :: Text -> [MappedKey] -> Text+makeToJSON identifier contents =+    Text.unlines [+        Text.concat ["instance ToJSON ", identifier, " where"]+      , Text.concat ["  toJSON     (", identifier, " {", wildcard, "}) = object [", inner ", ", "]"]+#if MIN_VERSION_aeson(0,11,0)+      , maybeToEncoding+#endif+      ]+  where+    maybeToEncoding | null contents = ""+                    | otherwise     =+                        Text.concat ["  toEncoding (", identifier, " {", wildcard, "}) = pairs  (", inner "<>", ")"]+    wildcard | null contents = ""+             | otherwise     = ".."+    inner separator = separator `Text.intercalate`+                      map putValue contents+    putValue (jsonId, haskellId, _typeText, _nullable) = Text.unwords [escapeText jsonId, ".=", haskellId]+    escapeText = Text.pack . show . Text.unpack+-- Contents example for wrapToJSON+--"hexValue"  .= hexValue+--                                        ,"colorName" .= colorName]+++-- | Makes a generic identifier name.+genericIdentifier :: DeclM Text+genericIdentifier = do+  i <- stepM+  return $! "Obj" `Text.append` tShow i++-- * Printing a single data type declaration+newDecl :: Text -> [(Text, Type)] -> DeclM Text+newDecl identifier kvs = do attrs <- forM kvs $ \(k, v) -> do+                              formatted <- formatType v+                              return (k, normalizeFieldName identifier k, formatted, isNullable v)+                            let decl = Text.unlines [wrapDecl     identifier $ fieldDecls attrs+                                                    ,""+                                                    ,makeFromJSON identifier              attrs+                                                    ,""+                                                    ,makeToJSON   identifier              attrs]+                            addDecl decl+                            return identifier+  where+    fieldDecls attrList = Text.intercalate ",\n" $ map fieldDecl attrList+    fieldDecl :: (Text, Text, Text, Bool) -> Text+    fieldDecl (_jsonName, haskellName, fType, _nullable) = Text.concat [+                                                             "    ", haskellName, " :: ", fType]++addDecl decl = decls %%= (\ds -> ((), decl:ds))++-- | Add new type alias for Array type+newAlias :: Text -> Type -> DeclM Text+newAlias identifier content = do formatted <- formatType content+                                 addDecl $ Text.unlines [wrapAlias identifier formatted]+                                 return identifier++-- | Convert a JSON key name given by second argument,+-- from within a dictionary keyed with first argument,+-- into a name of Haskell record field (hopefully distinct from other such selectors.)+normalizeFieldName ::  Text -> Text -> Text+normalizeFieldName identifier = escapeKeywords             .+                                uncapitalize               .+                                (normalizeTypeName identifier `Text.append`) .+                                normalizeTypeName++keywords ::  Set Text+keywords = Set.fromList ["type", "data", "module", "class", "where", "let", "do"]++escapeKeywords ::  Text -> Text+escapeKeywords k | k `Set.member` keywords = k `Text.append` "_"+escapeKeywords k                           = k++-- | Format the type within DeclM monad, that records+-- the separate declarations on which this one is dependent.+formatType :: Type -> DeclM Text+formatType  TString                          = return "Text"+formatType  TNum                             = return "Double"+formatType  TBool                            = return "Bool"+formatType (TLabel l)                        = return $ normalizeTypeName l+formatType (TUnion u)                        = wrap <$> case length nonNull of+                                                          0 -> return emptyTypeRepr+                                                          1 -> formatType $ head nonNull+                                                          _ -> Text.intercalate ":|:" <$> mapM formatType nonNull+  where+    nonNull       = Set.toList $ Set.filter (TNull /=) u+    wrap                                :: Text -> Text+    wrap   inner  | TNull `Set.member` u = Text.concat ["(Maybe (", inner, "))"]+                  | otherwise            =                          inner+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 Value)" -- default, accepts future extension where we found no data++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 (Map.toList $ unDict o) $ \(k, v) -> do+                                       component <- splitTypeByLabel' k v+                                       return (k, component)+                                    addType l (TObj $ Dict $ Map.fromList kvs)+                                    return $! TLabel l++-- | Splits initial type with a given label, into a mapping of object type names and object type structures.+splitTypeByLabel :: Text -> Type -> Map Text Type+splitTypeByLabel topLabel t = Map.map (foldl1' unifyTypes) finalState+  where+    finalize (TLabel l) = assert (l == topLabel) $ return ()+    finalize  topLevel  = addType topLabel topLevel+    initialState    = Map.empty+    (_, finalState) = runState (splitTypeByLabel' topLabel t >>= finalize) initialState++formatObjectType ::  Text -> Type -> DeclM Text+formatObjectType identifier (TObj o) = newDecl  identifier d+  where+    d = Map.toList $ unDict o+formatObjectType identifier  other   = newAlias identifier other++-- | Display an environment of types split by name.+displaySplitTypes ::  Map Text Type -> Text+displaySplitTypes dict = trace ("displaySplitTypes: " ++ show (toposort dict)) $ runDecl declarations+  where+    declarations =+      forM (toposort dict) $ \(name, typ) ->+        formatObjectType (normalizeTypeName name) typ++-- | Normalize type name by:+-- 1. Treating all characters that are not acceptable in Haskell variable name as end of word.+-- 2. Capitalizing each word, but a first (camelCase).+-- 3. Adding underscore if first character is non-alphabetic.+-- 4. Escaping Haskell keywords if the whole identifier is such keyword.+-- 5. If identifier is empty, then substituting "JsonEmptyKey" for its name.+normalizeTypeName :: Text -> Text+normalizeTypeName = ifEmpty "JsonEmptyKey"                  .+                    escapeKeywords                          .+                    escapeFirstNonAlpha                     .+                    Text.concat                             .+                    map capitalize                          .+                    filter     (not . Text.null)            .+                    Text.split (not . acceptableInVariable)+  where+    ifEmpty x ""       = x+    ifEmpty _ nonEmpty = nonEmpty+    acceptableInVariable c = isAlpha c || isDigit c+    escapeFirstNonAlpha cs                  | Text.null cs =                   cs+    escapeFirstNonAlpha cs@(Text.head -> c) | isAlpha   c  =                   cs+    escapeFirstNonAlpha cs                                 = "_" `Text.append` cs++-- | 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+
Data/Aeson/AutoType/Extract.hs view
@@ -32,7 +32,7 @@  -- | Compute total size of the type of the @Value@. -- For:--- * simple types it is always 1, +-- * simple types it is always 1, -- * for arrays it is just 1+_maximum_ size of the (single) element type, -- * for objects it is _sum_ of the sizes of fields (since each field type --   is assumed to be different.)@@ -99,7 +99,7 @@ unifyTypes  TNum       TNum      = TNum unifyTypes  TString    TString   = TString unifyTypes  TNull      TNull     = TNull-unifyTypes (TObj   d) (TObj   e) = TObj newDict +unifyTypes (TObj   d) (TObj   e) = TObj newDict   where     newDict :: Dict     newDict = Dict $ Map.fromList [(k, get k d `unifyTypes`
Data/Aeson/AutoType/Format.hs view
@@ -1,344 +1,17 @@-{-# LANGUAGE CPP                 #-}-{-# LANGUAGE TemplateHaskell     #-}-{-# LANGuaGE ScopedTypeVariables #-}-{-# LANGUAGE ViewPatterns        #-}-{-# LANGUAGE OverloadedStrings   #-}-{-# LANGuaGE DeriveGeneric       #-}-{-# LANGuaGE FlexibleContexts    #-}--- | Formatting type declarations and class instances for inferred types. -module Data.Aeson.AutoType.Format(-  displaySplitTypes, splitTypeByLabel, unificationCandidates,-  unifyCandidates,-  normalizeTypeName-) where--import           Control.Arrow             ((&&&))-import           Control.Applicative       ((<$>), (<*>))-import           Control.Lens.TH-import           Control.Lens-import           Control.Monad             (forM)-import           Control.Exception(assert)-import qualified Data.HashMap.Strict        as Map-import           Data.Monoid-import qualified Data.Set                   as Set-import qualified Data.Text                  as Text-import           Data.Text                 (Text)-import           Data.Set                  (Set )-import           Data.List                 (foldl1')-import           Data.Char                 (isAlpha, isDigit)-import           Control.Monad.State.Class-import           Control.Monad.State.Strict(State, runState)-import qualified Data.Graph          as Graph-import           GHC.Generics              (Generic)--import           Data.Aeson.AutoType.Type-import           Data.Aeson.AutoType.Extract-import           Data.Aeson.AutoType.Util  ()----import           Debug.Trace -- DEBUG-trace _ x = x--fst3 ::  (t, t1, t2) -> t-fst3 (a, _b, _c) = a--data DeclState = DeclState { _decls   :: [Text]-                           , _counter :: Int-                           }-  deriving (Eq, Show, Ord, Generic)--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 ---- | Wrap a type alias.-wrapAlias :: Text -> Text -> Text-wrapAlias identifier contents = Text.unwords ["type", identifier, "=", contents]---- | Wrap a data type declaration-wrapDecl ::  Text -> Text -> Text-wrapDecl identifier contents = Text.unlines [header, contents, "  } deriving (Show,Eq,GHC.Generics.Generic)"]-                                            --,"\nderiveJSON defaultOptions ''" `Text.append` identifier]-  where-    header = Text.concat ["data ", identifier, " = ", identifier, " { "]---- | Explanatory type alias for making declarations--- First element of the triple is original JSON identifier,--- second element of the triple is the mapped identifier name in Haskell.--- third element of the triple shows the type in a formatted way-type MappedKey = (Text, Text, Text, Bool)---- | Make ToJSON declaration, given identifier (object name in Haskell) and mapping of its keys--- from JSON to Haskell identifiers *in the same order* as in *data type declaration*.-makeFromJSON ::  Text -> [MappedKey] -> Text-makeFromJSON identifier contents =-  Text.unlines [-      Text.unwords ["instance FromJSON", identifier, "where"]-    , Text.unwords ["  parseJSON (Object v) =", makeParser identifier contents]-    , "  parseJSON _          = mzero" ]-  where-    makeParser identifier [] = Text.unwords ["return ", identifier]-    makeParser identifier _  = Text.unwords [identifier, "<$>", inner]-    inner                    = " <*> " `Text.intercalate`-                                  map takeValue contents-    takeValue (jsonId, _, ty, True ) = Text.concat ["v .:?? \"", jsonId, "\""] -- nullable types-    takeValue (jsonId, _, _ , False) = Text.concat ["v .:   \"", jsonId, "\""]--- Contents example for wrapFromJSON:--- " <$>---"                           v .: "hexValue"  <*>---"                           v .: "colorName\""---- | Make ToJSON declaration, given identifier (object name in Haskell) and mapping of its keys--- from JSON to Haskell identifiers in the same order as in declaration-makeToJSON :: Text -> [MappedKey] -> Text-makeToJSON identifier contents =-    Text.unlines [-        Text.concat ["instance ToJSON ", identifier, " where"]-      , Text.concat ["  toJSON     (", identifier, " {", wildcard, "}) = object [", inner ", ", "]"]-#if MIN_VERSION_aeson(0,11,0)-      , maybeToEncoding-#endif-      ]-  where-    maybeToEncoding | null contents = ""-                    | otherwise     =-                        Text.concat ["  toEncoding (", identifier, " {", wildcard, "}) = pairs  (", inner "<>", ")"]-    wildcard | null contents = ""-             | otherwise     = ".."-    inner separator = separator `Text.intercalate`-                      map putValue contents-    putValue (jsonId, haskellId, _typeText, _nullable) = Text.unwords [escapeText jsonId, ".=", haskellId]-    escapeText = Text.pack . show . Text.unpack--- Contents example for wrapToJSON---"hexValue"  .= hexValue---                                        ,"colorName" .= colorName]----- | Makes a generic identifier name.-genericIdentifier :: DeclM Text-genericIdentifier = do-  i <- stepM-  return $! "Obj" `Text.append` tShow i---- * Printing a single data type declaration-newDecl :: Text -> [(Text, Type)] -> DeclM Text-newDecl identifier kvs = do attrs <- forM kvs $ \(k, v) -> do-                              formatted <- formatType v-                              return (k, normalizeFieldName identifier k, formatted, isNullable v)-                            let decl = Text.unlines [wrapDecl     identifier $ fieldDecls attrs-                                                    ,""-                                                    ,makeFromJSON identifier              attrs-                                                    ,""-                                                    ,makeToJSON   identifier              attrs]-                            addDecl decl-                            return identifier-  where-    fieldDecls attrList = Text.intercalate ",\n" $ map fieldDecl attrList-    fieldDecl :: (Text, Text, Text, Bool) -> Text-    fieldDecl (_jsonName, haskellName, fType, _nullable) = Text.concat [-                                                             "    ", haskellName, " :: ", fType]--addDecl decl = decls %%= (\ds -> ((), decl:ds))---- | Add new type alias for Array type-newAlias :: Text -> Type -> DeclM Text-newAlias identifier content = do formatted <- formatType content-                                 addDecl $ Text.unlines [wrapAlias identifier formatted]-                                 return identifier---- | Convert a JSON key name given by second argument,--- from within a dictionary keyed with first argument,--- into a name of Haskell record field (hopefully distinct from other such selectors.)-normalizeFieldName ::  Text -> Text -> Text-normalizeFieldName identifier = escapeKeywords             .-                                uncapitalize               .-                                (normalizeTypeName identifier `Text.append`) .-                                normalizeTypeName--keywords ::  Set Text-keywords = Set.fromList ["type", "data", "module", "class", "where", "let", "do"]--escapeKeywords ::  Text -> Text-escapeKeywords k | k `Set.member` keywords = k `Text.append` "_"-escapeKeywords k                           = k---- | Format the type within DeclM monad, that records--- the separate declarations on which this one is dependent.-formatType :: Type -> DeclM Text-formatType  TString                          = return "Text"-formatType  TNum                             = return "Double"-formatType  TBool                            = return "Bool"-formatType (TLabel l)                        = return $ normalizeTypeName l-formatType (TUnion u)                        = wrap <$> case length nonNull of-                                                          0 -> return emptyTypeRepr-                                                          1 -> formatType $ head nonNull-                                                          _ -> Text.intercalate ":|:" <$> mapM formatType nonNull-  where-    nonNull       = Set.toList $ Set.filter (TNull /=) u-    wrap                                :: Text -> Text-    wrap   inner  | TNull `Set.member` u = Text.concat ["(Maybe (", inner, "))"]-                  | otherwise            =                          inner-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 Value)" -- default, accepts future extension where we found no data--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 (Map.toList $ unDict o) $ \(k, v) -> do-                                       component <- splitTypeByLabel' k v-                                       return (k, component)-                                    addType l (TObj $ Dict $ Map.fromList kvs)-                                    return $! TLabel l---- | Splits initial type with a given label, into a mapping of object type names and object type structures.-splitTypeByLabel :: Text -> Type -> Map Text Type-splitTypeByLabel topLabel t = Map.map (foldl1' unifyTypes) finalState-  where-    finalize (TLabel l) = assert (l == topLabel) $ return ()-    finalize  topLevel  = addType topLabel topLevel-    initialState    = Map.empty-    (_, finalState) = runState (splitTypeByLabel' topLabel t >>= finalize) initialState--formatObjectType ::  Text -> Type -> DeclM Text-formatObjectType identifier (TObj o) = newDecl  identifier d-  where-    d = Map.toList $ unDict o-formatObjectType identifier  other   = newAlias identifier other---- | Display an environment of types split by name.-displaySplitTypes ::  Map Text Type -> Text-displaySplitTypes dict = trace ("displaySplitTypes: " ++ show (toposort dict)) $ runDecl declarations-  where-    declarations =-      forM (toposort dict) $ \(name, typ) ->-        formatObjectType (normalizeTypeName name) typ+-- | Formatting tools for code generation.+module Data.Aeson.AutoType.Format(capitalize, uncapitalize) where --- | Normalize type name by:--- 1. Treating all characters that are not acceptable in Haskell variable name as end of word.--- 2. Capitalizing each word, but a first (camelCase).--- 3. Adding underscore if first character is non-alphabetic.--- 4. Escaping Haskell keywords if the whole identifier is such keyword.--- 5. If identifier is empty, then substituting "JsonEmptyKey" for its name.-normalizeTypeName :: Text -> Text-normalizeTypeName s  = ifEmpty "JsonEmptyKey"                  .-                       escapeKeywords                          .-                       escapeFirstNonAlpha                     .-                       Text.concat                             .-                       map capitalize                          .-                       filter     (not . Text.null)            .-                       Text.split (not . acceptableInVariable) $ s-  where-    ifEmpty x ""       = x-    ifEmpty _ nonEmpty = nonEmpty-    acceptableInVariable c = isAlpha c || isDigit c-    escapeFirstNonAlpha cs                  | Text.null cs =                   cs-    escapeFirstNonAlpha cs@(Text.head -> c) | isAlpha   c  =                   cs-    escapeFirstNonAlpha cs                                 = "_" `Text.append` cs+import Data.Text(Text)+import qualified Data.Text as Text +-- | Make the first letter of a Text upper case. capitalize :: Text -> Text capitalize word = Text.toUpper first `Text.append` rest   where     (first, rest) = Text.splitAt 1 word +-- | Make the first letter of a Text lower case. 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 (++) .-                        concatMap entry       .-                        Map.toList-  where-    -- | Candidate entry has to have at least two candidates, so that unification makes sense-    candidates [ ] = False-    candidates [_] = False-    candidates _   = True-    -- | Make a candidate entry for each object type, which points from its keys to its label.-    entry (k, TObj o)                 = [(Set.fromList $ Map.keys $ unDict o, [k])]-    entry  _                          = [] -- ignore array elements and toplevel type if it is Array---- | 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 candidate set 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 candidate set 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-
Data/Aeson/AutoType/Pretty.hs view
@@ -10,7 +10,7 @@ #endif  -- | Instances of @Text.PrettyPrint.Out@ class to visualize--- Aeson @Value@ data structure. +-- Aeson @Value@ data structure. module Data.Aeson.AutoType.Pretty() where  import qualified Data.HashMap.Strict as Hash@@ -49,13 +49,9 @@   docPrec _                 = doc  instance (Out a, Out b) => Out (HashMap a b) where-  doc (Hash.toList -> dict) = (foldl ($$) "{" $-                                 map formatPair dict)-                           $$  nest 1 "}"-                      +  doc (Hash.toList -> dict) = foldl ($$) "{" (map formatPair dict) $$ nest 1 "}"   docPrec _ = doc  instance Out Text where   doc       = text . Text.unpack -- TODO: check if there may be direct way?   docPrec _ = doc-
+ Data/Aeson/AutoType/Split.hs view
@@ -0,0 +1,143 @@+{-# LANGUAGE CPP                 #-}+{-# LANGUAGE TemplateHaskell     #-}+{-# LANGuaGE ScopedTypeVariables #-}+{-# LANGUAGE ViewPatterns        #-}+{-# LANGUAGE OverloadedStrings   #-}+{-# LANGuaGE DeriveGeneric       #-}+{-# LANGuaGE FlexibleContexts    #-}+-- | Formatting type declarations and class instances for inferred types. +module Data.Aeson.AutoType.Split(+  splitTypeByLabel, unificationCandidates,+  unifyCandidates+) where++import           Control.Arrow             ((&&&))+import           Control.Applicative       ((<$>), (<*>))+import           Control.Lens.TH+import           Control.Lens+import           Control.Monad             (forM)+import           Control.Exception(assert)+import qualified Data.HashMap.Strict        as Map+import           Data.Monoid+import qualified Data.Set                   as Set+import qualified Data.Text                  as Text+import           Data.Text                 (Text)+import           Data.Set                  (Set )+import           Data.List                 (foldl1')+import           Data.Char                 (isAlpha, isDigit)+import           Control.Monad.State.Class+import           Control.Monad.State.Strict(State, runState)+import qualified Data.Graph          as Graph+import           GHC.Generics              (Generic)++import           Data.Aeson.AutoType.Type+import           Data.Aeson.AutoType.Extract+import           Data.Aeson.AutoType.Util  ()++--import           Debug.Trace -- DEBUG+trace _ x = x++fst3 ::  (t, t1, t2) -> t+fst3 (a, _b, _c) = a++type Map k v = Map.HashMap k v ++-- | Explanatory type alias for making declarations+-- First element of the triple is original JSON identifier,+-- second element of the triple is the mapped identifier name in Haskell.+-- third element of the triple shows the type in a formatted way+type MappedKey = (Text, Text, Text, Bool)++-- * 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 (Map.toList $ unDict o) $ \(k, v) -> do+                                       component <- splitTypeByLabel' k v+                                       return (k, component)+                                    addType l (TObj $ Dict $ Map.fromList kvs)+                                    return $! TLabel l++-- | Splits initial type with a given label, into a mapping of object type names and object type structures.+splitTypeByLabel :: Text -> Type -> Map Text Type+splitTypeByLabel topLabel t = Map.map (foldl1' unifyTypes) finalState+  where+    finalize (TLabel l) = assert (l == topLabel) $ return ()+    finalize  topLevel  = addType topLabel topLevel+    initialState    = Map.empty+    (_, finalState) = runState (splitTypeByLabel' topLabel t >>= finalize) initialState++-- | 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 (++) .+                        concatMap entry       .+                        Map.toList+  where+    -- | Candidate entry has to have at least two candidates, so that unification makes sense+    candidates [ ] = False+    candidates [_] = False+    candidates _   = True+    -- | Make a candidate entry for each object type, which points from its keys to its label.+    entry (k, TObj o)                 = [(Set.fromList $ Map.keys $ unDict o, [k])]+    entry  _                          = [] -- ignore array elements and toplevel type if it is Array++-- | 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 candidate set 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 candidate set 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+
Data/Aeson/AutoType/Test.hs view
@@ -90,4 +90,3 @@  -- This one is generated with Generics and instances above instance Monad m => Serial m Value-
Data/Aeson/AutoType/Type.hs view
@@ -87,11 +87,11 @@  -- | Empty type emptyType :: Type-emptyType = TUnion Set.empty +emptyType = TUnion Set.empty  -- | Lookup the Type within the dictionary. get :: Text -> Dict -> Type-get key = Hash.lookupDefault TNull key . unDict +get key = Hash.lookupDefault TNull key . unDict  -- $derive makeUniplateDirect ''Type @@ -149,10 +149,9 @@ hasNonTopTObj (TObj o) = any hasTObj $ Hash.elems $ unDict o hasNonTopTObj _        = False --- | Check if the given type has TObj on top or within array.. +-- | Check if the given type has TObj on top or within array.. hasTObj           :: Type -> Bool hasTObj (TObj   _) = True hasTObj (TArray a) = hasTObj a hasTObj (TUnion u) = any hasTObj u hasTObj _          = False-
Data/Aeson/AutoType/Util.hs view
@@ -1,11 +1,11 @@ -- | Utility functions that may be ultimately moved to some library. module Data.Aeson.AutoType.Util( withFileOrHandle                                , withFileOrDefaultHandle-                               , assertM ) where+                               ) where  import           Data.Hashable import qualified Data.Set as Set-import           Control.Exception(assert)+import           Control.Exception --(assert) import           System.IO                 (withFile, IOMode(..), Handle, stdin, stdout)  -- | Generic function for opening file if the filename is not empty nor "-",@@ -14,7 +14,7 @@ withFileOrHandle :: FilePath -> IOMode -> Handle -> (Handle -> IO r) -> IO r withFileOrHandle        ""       _         handle action =                      action handle withFileOrHandle        "-"      _         handle action =                      action handle-withFileOrHandle        name     ioMode    _      action = withFile name ioMode action +withFileOrHandle        name     ioMode    _      action = withFile name ioMode action  -- | Generic function for choosing either file with given name or stdin/stdout as input/output. -- It accepts the function that takes the corresponding handle.@@ -24,16 +24,9 @@ withFileOrDefaultHandle "-"      WriteMode        action = action stdout withFileOrDefaultHandle "-"      otherMode        _      = error $ "Incompatible io mode ("                                                                 ++ show otherMode-                                                                ++ ") for `-` in withFileOrDefaultHandle." +                                                                ++ ") for `-` in withFileOrDefaultHandle." withFileOrDefaultHandle filename ioMode           action = withFile filename ioMode action --- | Check assertion within any monad.-assertM ::  Monad m => Bool -> m ()-assertM v = assert v $ return ()- -- Missing instances instance Hashable a => Hashable (Set.Set a) where   hashWithSalt = Set.foldr (flip hashWithSalt)-- - 
GenerateJSONParser.hs view
@@ -1,10 +1,12 @@ {-# LANGUAGE ScopedTypeVariables  #-} {-# LANGUAGE OverloadedStrings    #-}+{-# LANGUAGE NamedFieldPuns       #-} {-# LANGUAGE ViewPatterns         #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where  import           Control.Applicative+import           Control.Exception              (assert) import           Control.Monad                  (forM_, when, unless) import           Data.Maybe import           Data.Monoid@@ -22,11 +24,11 @@ import           Data.Text                      (Text) import           Text.PrettyPrint.GenericPretty (pretty) ---import           Data.Aeson.AutoType.Pretty-import           Data.Aeson.AutoType.Type+import           Data.Aeson.AutoType.CodeGen import           Data.Aeson.AutoType.Extract import           Data.Aeson.AutoType.Format-import           Data.Aeson.AutoType.CodeGen+import           Data.Aeson.AutoType.Split+import           Data.Aeson.AutoType.Type import           Data.Aeson.AutoType.Util import qualified Data.Yaml as Yaml @@ -34,34 +36,35 @@ import           CommonCLI  -- * Command line flags---defineFlag "o:outputFilename"  defaultOutputFilename "Write output to the given file"---defineFlag "suggest"           True                  "Suggest candidates for unification"---defineFlag "autounify"         True                  "Automatically unify suggested candidates"---defineFlag "t:test"            False                 "Try to run generated parser after"---defineFlag "d:debug"           False                 "Set this flag to see more debugging info"---defineFlag "y:typecheck"       True                  "Set this flag to typecheck after unification"---defineFlag "yaml"              False                 "Parse inputs as YAML instead of JSON"---defineFlag "p:preprocessor"    False                 "Work as GHC preprocessor (skip preprocessor pragma)"---defineFlag "fakeFlag"          True                  "Ignore this flag - it doesn't exist!!! It is workaround to library problem."- data Options = Options {                  tyOpts :: TypeOpts                , outputFilename :: FilePath                , typecheck :: Bool                , yaml :: Bool                , preprocessor :: Bool+               , lang :: Lang                , filenames :: [FilePath]                }  optParser :: Parser Options optParser  =     Options  <$> tyOptParser-             <*> strOption (short 'o' <> long "output" <> long "outputFilename" <> value defaultOutputFilename)-             <*> unflag    (short 'n' <> long "no-typecheck" <> help "Do not typecheck after unification")-             <*> switch    (long  "yaml"                  <> help "Parse inputs as YAML instead of JSON"  )-             <*> switch    (short 'p' <> long "preprocessor" <> help "Work as GHC preprocessor (and skip preprocessor pragma)"  )+             <*> strOption (short 'o'             <>+                            long "output"         <>+                            long "outputFilename" <> value "")+             <*> unflag    (short 'n'             <>+                            long "no-typecheck"   <> help "Do not typecheck after unification")+             <*> switch    (long  "yaml"          <> help "Parse inputs as YAML instead of JSON")+             <*> switch    (short 'p'             <>+                            long "preprocessor"   <>+                              help "Work as GHC preprocessor (and skip preprocessor pragma)")+             <*> langOpts              <*> some (argument str (metavar "FILES...")) +langOpts :: Parser Lang+langOpts  =  flag Haskell Haskell (long "haskell")+         <|> flag Haskell Elm     (long "elm")+ -- | Report an error to error output. report   :: Text -> IO () report    = Text.hPutStrLn stderr@@ -119,11 +122,11 @@  -- | Take a set of JSON input filenames, Haskell output filename, and generate module parsing these JSON files. generateHaskellFromJSONs :: Options -> [FilePath] -> FilePath -> IO ()-generateHaskellFromJSONs opts inputFilenames outputFilename = do+generateHaskellFromJSONs opts@Options { tyOpts=TyOptions { toplevel } } inputFilenames outputFilename = do   -- Read type from each file   (filenames,    typeForEachFile,-   valueForEachFile) <- (unzip3 . catMaybes) <$> mapM (extractTypeFromJSONFile opts) inputFilenames+   valueForEachFile) <- unzip3 . catMaybes <$> mapM (extractTypeFromJSONFile opts) inputFilenames   -- Unify all input types   when (null typeForEachFile) $ do     report "No valid JSON input file..."@@ -133,28 +136,29 @@                         then typeChecking finalType filenames valueForEachFile                         else return                 filenames   -- We split different dictionary labels to become different type trees (and thus different declarations.)-  let splitted = splitTypeByLabel "TopLevel" finalType+  let splitted = splitTypeByLabel toplevelName finalType   myTrace $ "SPLITTED: " ++ pretty splitted-  assertM $ not $ any hasNonTopTObj $ Map.elems splitted-  -- We compute which type labels are candidates for unification-  let uCands = unificationCandidates splitted-  myTrace $ "CANDIDATES:\n" ++ pretty uCands-  when (suggest $ tyOpts opts) $ forM_ uCands $ \cs -> do-                         putStr "-- "-                         Text.putStrLn $ "=" `Text.intercalate` cs-  -- We unify the all candidates or only those that have been given as command-line flags.-  let unified = if autounify $ tyOpts opts-                  then unifyCandidates uCands splitted-                  else splitted-  myTrace $ "UNIFIED:\n" ++ pretty unified-  -- We start by writing module header-  writeHaskellModule outputFilename unified-  when (test $ tyOpts opts) $-    exitWith =<< runghc (outputFilename:passedTypeCheck)+  assert (not $ any hasNonTopTObj $ Map.elems splitted) $ do+    -- We compute which type labels are candidates for unification+    let uCands = unificationCandidates splitted+    myTrace $ "CANDIDATES:\n" ++ pretty uCands+    when (suggest $ tyOpts opts) $ forM_ uCands $ \cs -> do+                           putStr "-- "+                           Text.putStrLn $ "=" `Text.intercalate` cs+    -- We unify the all candidates or only those that have been given as command-line flags.+    let unified = if autounify $ tyOpts opts+                    then unifyCandidates uCands splitted+                    else splitted+    myTrace $ "UNIFIED:\n" ++ pretty unified+    -- We start by writing module header+    writeModule (lang opts) outputFilename toplevelName unified+    when (test $ tyOpts opts) $+      exitWith =<< runModule (lang opts) (outputFilename:passedTypeCheck)   where     -- | Works like @Debug.trace@ when the --debug flag is enabled, and does nothing otherwise.     myTrace :: String -> IO ()     myTrace msg = debug (tyOpts opts) `when` putStrLn msg+    toplevelName = capitalize $ Text.pack toplevel  -- | Drop initial pragma. dropPragma :: BSL.ByteString -> BSL.ByteString@@ -168,6 +172,6 @@           generateHaskellFromJSONs opts (filenames opts) (outputFilename opts)   where     optInfo = info (optParser <**> helper)-            ( fullDesc+            (  fullDesc             <> progDesc "Parser JSON or YAML, get its type, and generate appropriate parser."             <> header "json-autotype -- automatic type and parser generation from JSON")
GenerateTestJSON.hs view
@@ -1,10 +1,7 @@-{-# LANGUAGE TemplateHaskell      #-} {-# LANGUAGE ScopedTypeVariables  #-} {-# LANGUAGE OverloadedStrings    #-}-{-# LANGUAGE DeriveGeneric        #-}-{-# LANGUAGE StandaloneDeriving   #-}+{-# LANGUAGE NamedFieldPuns       #-} {-# LANGUAGE RecordWildCards      #-}-{-# LANGUAGE ViewPatterns         #-} {-# LANGUAGE FlexibleInstances    #-} {-# LANGUAGE FlexibleContexts     #-} {-# OPTIONS_GHC -fno-warn-orphans #-}@@ -19,6 +16,7 @@ import           System.Directory          (removeFile) import           System.Process            (system) import           Control.Monad             (forM_, forM, when)+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@@ -141,7 +139,8 @@  -- | Take a set of JSON input filenames, Haskell output filename, and generate module parsing these JSON files. generateTestJSONs :: Options -> IO ()-generateTestJSONs Options {tyOpts=TyOptions {..}, ..}= do+generateTestJSONs Options {tyOpts=TyOptions {..},+                           ..}= do     testValues :: [Value] <- generate $                                resize size $                                  vectorWithoutDuplicates 100 arbitraryTopValue@@ -156,29 +155,29 @@           exitFailure         let finalType = foldr1 unifyTypes typeForEachFile         -- We split different dictionary labels to become different type trees (and thus different declarations.)-        let splitted = splitTypeByLabel "TopLevel" finalType+        let splitted = splitTypeByLabel toplevelName finalType         --myTrace $ "SPLITTED: " ++ pretty splitted-        assertM $ not $ any hasNonTopTObj $ Map.elems splitted-        -- We compute which type labels are candidates for unification-        let uCands = unificationCandidates splitted-        myTrace $ "CANDIDATES:\n" ++ pretty uCands-        when suggest $ forM_ uCands $ \cs -> do-                               putStr "-- "-                               Text.putStrLn $ "=" `Text.intercalate` cs-        -- We unify the all candidates or only those that have been given as command-line flags.-        let unified = if autounify-                        then unifyCandidates uCands splitted-                        else splitted-        myTrace $ "UNIFIED:\n" ++ pretty unified-        -- We start by writing module header-        writeHaskellModule outputFilename unified-        if test-          then do-            r <- (==ExitSuccess) <$> runghc [outputFilename, inputFilename]-            when r $ mapM_ removeFile [inputFilename, outputFilename]-            return r-          else-            return True+        assert (not $ any hasNonTopTObj $ Map.elems splitted) $ do+          -- We compute which type labels are candidates for unification+          let uCands = unificationCandidates splitted+          myTrace $ "CANDIDATES:\n" ++ pretty uCands+          when suggest $ forM_ uCands $ \cs -> do+                                 putStr "-- "+                                 Text.putStrLn $ "=" `Text.intercalate` cs+          -- We unify the all candidates or only those that have been given as command-line flags.+          let unified = if autounify+                          then unifyCandidates uCands splitted+                          else splitted+          myTrace $ "UNIFIED:\n" ++ pretty unified+          -- We start by writing module header+          writeHaskellModule outputFilename toplevelName unified+          if test+            then do+              r <- (==ExitSuccess) <$> runModule (lang opts) [outputFilename, inputFilename]+              when r $ mapM_ removeFile [inputFilename, outputFilename]+              return r+            else+              return True     putStrLn $ "Successfully generated "      ++ show (length results) ++                " JSON files, out of planned " ++ show count  ++ " cases."   where@@ -188,6 +187,7 @@     outputFilenames    = map makeOutputFilename [1..count]     myTrace :: String -> IO ()     myTrace msg = debug `when` putStrLn msg+    toplevelName = capitalize $ Text.pack toplevel  main :: IO () main = do opts <- execParser optInfo
README.md view
@@ -113,4 +113,10 @@ Other approaches: ================= -There is a [json-sampler](https://maxs.io/generating-types-from-json-samples/) that allows to make simpler data structure from JSON examples, but doesn't seem to perform unification, nor is it suitable for big APIs.+* There is a [TypeScript type provider](https://jvilk.com/MakeTypes/), and [PLDI 2016 paper](https://dl.acm.org/citation.cfm?id=2908115) on solving this problem using <em>preferred type shapes</em> instead of union types.+One can think about it as a alternative theory that gives very similar results, with more complicated exposition. It also does not tackle the problem of tagged records. It also does not attempt to <em>guess</em> unification candidates in order to reduce type complexity.+* There *was* a [json-sampler](https://maxs.io/generating-types-from-json-samples/) that allows to make simpler data structure from JSON examples, but doesn't seem to perform unification, nor is it suitable for big APIs.++* [PADS project](https://www.cs.princeton.edu/~dpw/papers/padsml06.pdf) is another attempt to automatically infer types to treat <em>arbitrary</em> data formats (not just JSON). It mixes type declarations, with parsing/printing information in order to have a consistent view of both. It does not handle automatic type inference though.+* [JSON Schema generator](https://www.newtonsoft.com/jsonschema/help/html/GenerateSchema.htm) uses .NET types to generate JSON Schema instead (in opposite direction.) Similar schema generation is [used here](https://sixgun.wordpress.com/2012/02/09/using-json-net-to-generate-jsonschema/)+* Microsoft Developer Network advocates use of [Data Contracts](https://docs.microsoft.com/en-us/dotnet/framework/wcf/feature-details/using-data-contracts) instead to constrain possible input data.
changelog.md view
@@ -1,5 +1,11 @@ Changelog =========+    1.1.0  Mar 2018+        * Partial support Elm code generation.++    1.0.19  Nov 2017+        * Allow to have a custom name for toplevel data type.+     1.0.18  Nov 2017         * Fixed unit tests.         * Fixed import for inclusion in Stackage.@@ -108,11 +114,11 @@     0.2.5.7  Mar 2015          * Fixed documentation anchors, and unit test classification for failures.-    +     0.2.5.6  Mar 2015          * Relaxed upper bounds for lens 4.8.-    +     0.2.5.5  Mar 2015          * (Skipped this version number by mistake.)@@ -173,4 +179,3 @@  	* First experiments uploaded to GitHub, and discussed to 	HackerSpace.SG.-
json-autotype.cabal view
@@ -1,6 +1,6 @@ -- Build information for the package. name:                json-autotype-version:             1.0.18+version:             1.1.0 synopsis:            Automatic type declaration for JSON input data description:         Generates datatype declarations with Aeson's "FromJSON" instances                      from a set of example ".json" files.@@ -20,6 +20,10 @@                      .                      One can now use multiple input files to generate better type description.                      .+                     Now with Elm code generation support!+                     (If you want your favourite programming language supported too -+                     name your price and mail the author.)+                     .                      See introduction on  <https://github.com/mgajda/json-autotype>                      for details. homepage:            https://github.com/mgajda/json-autotype@@ -34,8 +38,7 @@ extra-source-files:  README.md changelog.md cabal-version:       >=1.10 bug-reports:         https://github.com/mgajda/json-autotype/issues-tested-with:         GHC==7.8.4,GHC==7.10.3,GHC==7.6.3,GHC==8.0.1,GHC==8.2.2-+tested-with:         GHC==7.8.4, GHC==7.10.3, GHC==7.6.3, GHC==8.0.1, GHC==8.2.2, GHC==8.4.1 source-repository head   type:     git   location: https://github.com/mgajda/json-autotype.git@@ -60,16 +63,19 @@   other-modules:                        -- internal module                        Data.Aeson.AutoType.Util-  build-depends:       base                 >=4.3  && <4.11,+                       Data.Aeson.AutoType.Split+                       Data.Aeson.AutoType.CodeGen.Haskell+                       Data.Aeson.AutoType.CodeGen.Elm+                       Data.Aeson.AutoType.CodeGen.HaskellFormat+                       Data.Aeson.AutoType.CodeGen.ElmFormat+  build-depends:       base                 >=4.3  && <4.12,                        GenericPretty        >=1.2  && <1.3,                        aeson                >=0.7  && <1.3,-                       bytestring           >=0.9  && <0.11,                        containers           >=0.3  && <0.6,                        filepath             >=1.3  && <1.5,                        hashable             >=1.2  && <1.3,                        ---hint                 >=0.4  && <0.6,-                       optparse-applicative >=0.11 && <1.0,-                       lens                 >=4.1  && <4.16,+                       lens                 >=4.1  && <4.17,                        --mmap                 >=0.5  && <0.6,                        mtl                  >=2.1  && <2.3,                        pretty               >=1.1  && <1.3,@@ -87,7 +93,12 @@                        Data.Aeson.AutoType.Extract                        Data.Aeson.AutoType.Format                        Data.Aeson.AutoType.Pretty+                       Data.Aeson.AutoType.Split                        Data.Aeson.AutoType.CodeGen+                       Data.Aeson.AutoType.CodeGen.Haskell+                       Data.Aeson.AutoType.CodeGen.Elm+                       Data.Aeson.AutoType.CodeGen.HaskellFormat+                       Data.Aeson.AutoType.CodeGen.ElmFormat                        Data.Aeson.AutoType.Alternative                        -- Data.Aeson.AutoType.Plugin.Subtype                        -- Data.Aeson.AutoType.Plugin.Loader@@ -101,7 +112,7 @@                        DeriveDataTypeable,                        DeriveGeneric,                        RecordWildCards-  build-depends:       base                 >=4.3  && <4.11,+  build-depends:       base                 >=4.3  && <4.12,                        GenericPretty        >=1.2  && <1.3,                        aeson                >=0.7  && <1.3,                        bytestring           >=0.9  && <0.11,@@ -109,7 +120,7 @@                        filepath             >=1.3  && <1.5,                        hashable             >=1.2  && <1.3,                        --hint                 >=0.4  && <0.6,-                       lens                 >=4.1  && <4.16,+                       lens                 >=4.1  && <4.17,                        mtl                  >=2.1  && <2.3,                        optparse-applicative >=0.12 && <1.0,                        pretty               >=1.1  && <1.3,@@ -120,7 +131,7 @@                        unordered-containers >=0.2  && <0.3,                        vector               >=0.9  && <0.13,                        yaml                 >=0.8  && <0.9-  -- hs-source-dirs:      +  -- hs-source-dirs:   default-language:    Haskell2010  -- * Test suites@@ -131,6 +142,7 @@   main-is:             test/TestQC.hs   other-modules:       Data.Aeson.AutoType.Util                        Data.Aeson.AutoType.Extract+                       Data.Aeson.AutoType.Format                        Data.Aeson.AutoType.Test                        Data.Aeson.AutoType.Type                        Data.Aeson.AutoType.Pretty@@ -142,18 +154,12 @@                        DeriveDataTypeable,                        DeriveGeneric,                        RecordWildCards-  build-depends:       base                 >=4.3  && <4.11,+  build-depends:       base                 >=4.3  && <4.12,                        GenericPretty        >=1.2  && <1.3,                        aeson                >=0.7  && <1.3,-                       bytestring           >=0.9  && <0.11,                        containers           >=0.3  && <0.6,-                       directory            >=1.1  && <1.4,-                       filepath             >=1.3  && <1.5,                        hashable             >=1.2  && <1.3,-                       lens                 >=4.1  && <4.16,-                       mtl                  >=2.1  && <2.3,                        pretty               >=1.1  && <1.3,-                       process              >=1.1  && <1.7,                        scientific           >=0.3  && <0.5,                        smallcheck           >=1.0  && <1.2,                        text                 >=1.1  && <1.4,@@ -169,6 +175,7 @@   main-is:             test/TestExamples.hs   other-modules:       Data.Aeson.AutoType.Util                        Data.Aeson.AutoType.Extract+                       Data.Aeson.AutoType.Format                        Data.Aeson.AutoType.Test                        Data.Aeson.AutoType.Type                        Data.Aeson.AutoType.Pretty@@ -181,24 +188,21 @@                        DeriveDataTypeable,                        DeriveGeneric,                        RecordWildCards-  build-depends:       base                 >=4.3  && <4.11,+  build-depends:       base                 >=4.3  && <4.12,                        GenericPretty        >=1.2  && <1.3,                        aeson                >=0.7  && <1.3,-                       bytestring           >=0.9  && <0.11,                        containers           >=0.3  && <0.6,                        directory            >=1.1  && <1.4,                        filepath             >=1.3  && <1.5,                        hashable             >=1.2  && <1.3,-                       lens                 >=4.1  && <4.16,-                       mtl                  >=2.1  && <2.3,                        optparse-applicative >=0.11 && <1.0,                        pretty               >=1.1  && <1.3,                        process              >=1.1  && <1.7,                        scientific           >=0.3  && <0.5,                        smallcheck           >=1.0  && <1.2,                        text                 >=1.1  && <1.4,-                       uniplate             >=1.6  && <1.7,                        unordered-containers >=0.2  && <0.3,+                       uniplate             >=1.6  && <1.7,                        vector               >=0.9  && <0.13,                        QuickCheck           >=2.4  && <3.0   -- hs-source-dirs:@@ -211,9 +215,14 @@   other-modules:       Data.Aeson.AutoType.Util                        Data.Aeson.AutoType.Pretty                        Data.Aeson.AutoType.Type+                       Data.Aeson.AutoType.Split                        Data.Aeson.AutoType.Extract                        Data.Aeson.AutoType.Format                        Data.Aeson.AutoType.CodeGen+                       Data.Aeson.AutoType.CodeGen.Haskell+                       Data.Aeson.AutoType.CodeGen.Elm+                       Data.Aeson.AutoType.CodeGen.HaskellFormat+                       Data.Aeson.AutoType.CodeGen.ElmFormat                        Data.Aeson.AutoType.Test                        CommonCLI   other-extensions:    TemplateHaskell,@@ -224,7 +233,7 @@                        DeriveDataTypeable,                        DeriveGeneric,                        RecordWildCards-  build-depends:       base                 >=4.3  && <4.11,+  build-depends:       base                 >=4.3  && <4.12,                        GenericPretty        >=1.2  && <1.3,                        aeson                >=0.7  && <1.3,                        bytestring           >=0.9  && <0.11,@@ -233,7 +242,7 @@                        filepath             >=1.3  && <1.5,                        hashable             >=1.2  && <1.3,                        optparse-applicative >=0.12 && <1.0,-                       lens                 >=4.1  && <4.16,+                       lens                 >=4.1  && <4.17,                        mtl                  >=2.1  && <2.3,                        pretty               >=1.1  && <1.3,                        process              >=1.1  && <1.7,@@ -244,6 +253,5 @@                        unordered-containers >=0.2  && <0.3,                        vector               >=0.9  && <0.13,                        QuickCheck           >=2.4  && <3.0-  -- hs-source-dirs:      +  -- hs-source-dirs:   default-language:    Haskell2010-
test/TestExamples.hs view
@@ -1,13 +1,13 @@ -- Test over all files in examples/ directory module Main(main) where -import Control.Monad(forM)+import Control.Monad(forM, forM_, unless) import Data.Char(toUpper) import Data.Functor ((<$>)) import Data.List(isPrefixOf, isSuffixOf) import System.Directory(doesDirectoryExist, getDirectoryContents)-import System.FilePath(dropExtension, (</>), (<.>))-import System.Exit(exitSuccess, exitWith, ExitCode(..))+import System.FilePath((</>), (<.>), takeBaseName, replaceFileName)+import System.Exit(ExitCode(..))  import CommonCLI @@ -15,7 +15,7 @@ getRecursiveContents :: FilePath -> IO [FilePath] getRecursiveContents topdir = do   ex<-doesDirectoryExist topdir-  if ex +  if ex         then do           names <- getDirectoryContents topdir           let properNames = filter (not . isPrefixOf ".") names@@ -25,30 +25,22 @@             if isDirectory               then getRecursiveContents path               else return [path]-          return (concat paths) +          return (concat paths)         else return []  capitalize :: String -> String+capitalize [] = [] capitalize (s:ss) = toUpper s:ss  main :: IO () main  = do   filenames <-  filter (isSuffixOf ".json")             <$> getRecursiveContents "examples"-  results   <- forM filenames $ \filename -> do-    let outputFilename = capitalize (dropExtension filename) <.> "hs"+  forM_ filenames $ \filename -> do+    let outputFilename = filename `replaceFileName` capitalize (takeBaseName filename <.> "hs")     genResult <- runghc ["GenerateJSONParser.hs", filename, "--outputFilename", outputFilename]-    return 0-    if genResult == ExitSuccess-      then return 0 -- number of failures so far-      else do-        parserResult <- runghc [outputFilename, filename]-        if parserResult == ExitSuccess-           then return 0-           else return 1-  exitCode $ sum results--exitCode  :: Int -> IO ()-exitCode 0 = exitSuccess-exitCode n = exitWith $ ExitFailure n-+    unless (genResult == ExitSuccess) $+      fail (unwords ["test case", show filename, "failed with", show genResult])+    parserResult <- runghc [outputFilename, filename]+    unless (parserResult == ExitSuccess) $+      fail (unwords ["generated parser", show outputFilename, "failed with", show parserResult])