{-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE FlexibleInstances #-} {-# LANGUAGE FlexibleContexts #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main where import Control.Applicative import Control.Monad.State as State import Data.Maybe import System.Exit import System.IO (stdin, stderr, stdout, IOMode(..)) import System.FilePath (splitExtension, (<.>), ()) import System.Directory (removeFile, createDirectoryIfMissing) 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 import Data.Monoid ((<>)) import Data.Aeson (Value(..), eitherDecode, encode, FromJSON(..), ToJSON(..)) import Data.Function (on) import Data.List import qualified Data.Text as Text import qualified Data.Text.IO as Text import Data.Text (Text) import qualified Data.Vector as V import Data.Scientific (scientific, Scientific) import Text.PrettyPrint.GenericPretty (pretty) import Test.QuickCheck import Data.Aeson.AutoType.CodeGen(writeModule, runModule, Lang(..)) import Data.Aeson.AutoType.Extract import Data.Aeson.AutoType.Format import Data.Aeson.AutoType.Pretty import Data.Aeson.AutoType.Split import Data.Aeson.AutoType.Test import Data.Aeson.AutoType.Type import Data.Aeson.AutoType.Util import Options.Applicative import CommonCLI data Options = Options { tyOpts :: TypeOpts , keep :: Bool , stem :: FilePath , count :: Int , size :: Int } optParser :: Parser Options optParser = Options <$> tyOptParser <*> switch (long "keep" <> help "Also keep successful tests" ) <*> strOption (long "stem" <> value "Test" <> help "Output filename stem" ) <*> intOpt (long "count" <> value 100 <> help "Number of tests to perform" ) <*> intOpt (long "size" <> value 10 <> help "size of generated test cases") -- <*> some (argument str (metavar "FILES...")) where intOpt = option auto -- | Report an error to error output. report :: Text -> IO () report = Text.hPutStrLn stderr -- | Report an error and terminate the program. fatal :: Text -> IO () fatal msg = do report msg exitFailure -- | Read JSON and extract @Type@ information from it. extractTypeFromJSONFile :: (String -> IO ()) -> FilePath -> IO (Maybe Type) extractTypeFromJSONFile myTrace inputFilename = withFileOrHandle inputFilename ReadMode stdin $ \hIn -> -- First we decode JSON input into Aeson's Value type do bs <- BSL.hGetContents hIn Text.hPutStrLn stderr $ "Processing " `Text.append` Text.pack (show inputFilename) case eitherDecode bs of Left err -> do report $ Text.concat ["Cannot decode JSON input from " ,Text.pack (show inputFilename) ,"\n" , Text.pack err] return Nothing Right v -> do -- If decoding JSON was successful... -- We extract type structure from the JSON value. let t = extractType v --myTrace $ "Type: " ++ pretty t return $ Just t vectorWithoutDuplicates :: Ord b => Int -> Gen b -> Gen [b] vectorWithoutDuplicates i gen = take i . removeDuplicates <$> infiniteListOf gen removeDuplicates :: Ord a => [a] -> [a] removeDuplicates list = filterM checkDup list `evalState` Set.empty where checkDup x = do seen <- State.get if x `Set.member` seen then return False else do State.put $ x `Set.insert` seen return True -- TODO: check for generic Ord? instance Ord Value where Null `compare` Null = EQ Null `compare` _ = LT _ `compare` Null = GT (Bool a) `compare` (Bool b) = a `compare` b (Bool a) `compare` _ = LT _ `compare` (Bool b) = GT (Number a) `compare` (Number b) = a `compare` b (Number _) `compare` _ = LT _ `compare` (Number _) = GT (String a) `compare` (String b) = a `compare` b (String a) `compare` _ = LT _ `compare` (String b) = GT (Array a) `compare` (Array b) = a `compare` b (Array a) `compare` _ = LT _ `compare` (Array b) = GT (Object a) `compare` (Object b) = Map.toList a `compare` Map.toList b -- | 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 createDirectoryIfMissing True "output" testValues :: [Value] <- generate $ resize size $ vectorWithoutDuplicates 100 arbitraryTopValue results <- forM (zip3 inputFilenames outputFilenames testValues) $ \(inputFilename, outputFilename, jsonValue) -> do BSL.writeFile inputFilename $ encode jsonValue -- Read type from each file typeForEachFile <- catMaybes <$> mapM (extractTypeFromJSONFile myTrace) [inputFilename] -- Unify all input types when (null typeForEachFile) $ do report "No valid JSON input file..." exitFailure let finalType = foldr1 unifyTypes typeForEachFile -- We split different dictionary labels to become different type trees (and thus different declarations.) let splitted = splitTypeByLabel toplevelName finalType --myTrace $ "SPLITTED: " ++ pretty splitted 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 writeModule lang outputFilename toplevelName unified if test then do r <- (==ExitSuccess) <$> runModule lang [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 makeInputFilename = (<.>".json") . (stem ++) . show makeOutputFilename = ("output") . (<.>".hs") . (stem ++) . show inputFilenames = map makeInputFilename [1..count] 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 generateTestJSONs opts where optInfo = info (optParser <**> helper) ( fullDesc <> progDesc "Generate a number of JSON test files, and generate type and parser for each." <> header "Self-test for json-autotype" )