json-autotype 0.2.5.13 → 0.3
raw patch · 10 files changed
+184/−79 lines, 10 filesdep +smallcheckPVP ok
version bump matches the API change (PVP)
Dependencies added: smallcheck
API changes (from Hackage documentation)
+ Data.Aeson.AutoType.Type: emptySetLikes :: Set Type
+ Data.Aeson.AutoType.Type: isNullable :: Type -> Bool
Files
- Data/Aeson/AutoType/Alternative.hs +2/−0
- Data/Aeson/AutoType/Format.hs +13/−21
- Data/Aeson/AutoType/Pretty.hs +6/−2
- Data/Aeson/AutoType/Test.hs +30/−6
- Data/Aeson/AutoType/Type.hs +29/−5
- GenerateTestJSON.hs +75/−34
- README.md +2/−2
- TestQC.hs +17/−7
- changelog.md +5/−0
- json-autotype.cabal +5/−2
Data/Aeson/AutoType/Alternative.hs view
@@ -21,11 +21,13 @@ toEither :: a :|: b -> Either a b toEither (AltLeft a) = Left a toEither (AltRight b) = Right b+{-# INLINE toEither #-} -- | Convert from @Either@ datatype. fromEither :: Either a b -> a :|: b fromEither (Left a) = AltLeft a fromEither (Right b) = AltRight b+{-# INLINE fromEither #-} -- | Deconstruct the type with two functions corresponding to constructors. -- This is like @either@.
Data/Aeson/AutoType/Format.hs view
@@ -68,7 +68,7 @@ -- 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 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*.@@ -83,8 +83,8 @@ makeParser identifier _ = Text.unwords [identifier, "<$>", inner] inner = " <*> " `Text.intercalate` map takeValue contents- takeValue (jsonId, _, ty) | isNullable ty = Text.concat ["v .:? \"", jsonId, "\""]- takeValue (jsonId, _, _ ) = Text.concat ["v .: \"", jsonId, "\""]+ takeValue (jsonId, _, ty, True ) = Text.concat ["v .:? \"", jsonId, "\""] -- nullable types+ takeValue (jsonId, _, _ , False) = Text.concat ["v .: \"", jsonId, "\""] -- Contents example for wrapFromJSON: -- " <$> --" v .: "hexValue" <*>@@ -103,7 +103,7 @@ | otherwise = ".." inner = ", " `Text.intercalate` map putValue contents- putValue (jsonId, haskellId, _typeText) = Text.unwords [escapeText jsonId, ".=", haskellId]+ putValue (jsonId, haskellId, _typeText, _nullable) = Text.unwords [escapeText jsonId, ".=", haskellId] escapeText = Text.pack . show . Text.unpack -- Contents example for wrapToJSON --"hexValue" .= hexValue@@ -120,22 +120,19 @@ 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)+ return (k, normalizeFieldName identifier k, formatted, isNullable v) let decl = Text.unlines [wrapDecl identifier $ fieldDecls attrs ,""- ,makeFromJSON identifier attrs+ ,makeFromJSON identifier attrs ,""- ,makeToJSON identifier attrs]+ ,makeToJSON identifier attrs] addDecl decl return identifier where fieldDecls attrList = Text.intercalate ",\n" $ map fieldDecl attrList- fieldDecl :: (Text, Text, Text) -> Text- fieldDecl (_jsonName, haskellName, fType) = Text.concat [- " ", haskellName, " :: ", fType]---- | Whether the type is nullable or in other words - represented by Maybe type.-isNullable = ("Maybe" `Text.isPrefixOf`)+ fieldDecl :: (Text, Text, Text, Bool) -> Text+ fieldDecl (_jsonName, haskellName, fType, _nullable) = Text.concat [+ " ", haskellName, " :: ", fType] addDecl decl = decls %%= (\ds -> ((), decl:ds)) @@ -161,18 +158,13 @@ escapeKeywords k | k `Set.member` keywords = k `Text.append` "_" escapeKeywords k = k -emptySetLikes :: Set Type-emptySetLikes = Set.fromList [TNull, TArray $ TUnion $ Set.fromList []]- formatType :: Type -> DeclM Text formatType TString = return "Text" formatType TNum = return "Int" formatType TBool = return "Bool" formatType (TLabel l) = return $ normalizeTypeName l-formatType (TUnion u) | uu <- u `Set.difference` emptySetLikes,- Set.size uu == 1 = do fmt <- formatType $ head $ Set.toList uu- return $ "Maybe " `Text.append` fmt--- TODO: Use Maybe (...) when nullable?+formatType (TUnion u) | (TNull==) `any` u = do fmt <- formatType $ head $ Set.toList $ Set.filter (TNull /=) u+ return $ Text.concat ["Maybe (", fmt, ")"] formatType (TUnion u) = do tys <- forM (Set.toList u) formatType return $ mkUnion tys where@@ -189,7 +181,7 @@ formatType t = return $ "ERROR: Don't know how to handle: " `Text.append` tShow t emptyTypeRepr :: Text-emptyTypeRepr = "Maybe Text" -- default...+emptyTypeRepr = "Maybe Value" -- default, accepts future extension where we found no data runDecl :: DeclM a -> Text runDecl decl = Text.unlines $ finalState ^. decls
Data/Aeson/AutoType/Pretty.hs view
@@ -1,5 +1,9 @@-{-# LANGUAGE TemplateHaskell, ScopedTypeVariables, OverloadedStrings #-}-{-# LANGUAGE DeriveGeneric, StandaloneDeriving, ViewPatterns #-}+{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE StandaloneDeriving #-}+{-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- | Instances of @Text.PrettyPrint.Out@ class to visualize
Data/Aeson/AutoType/Test.hs view
@@ -1,9 +1,12 @@-{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE MultiParamTypeClasses #-} -- | Arbitrary instances for the JSON @Value@. module Data.Aeson.AutoType.Test ( arbitraryTopValue ) where +import Data.Aeson.AutoType.Pretty () -- Generic instance for Value+ import Control.Applicative ((<$>), (<*>)) import Data.Aeson import Data.Function (on)@@ -14,9 +17,11 @@ import Data.Text (Text) import qualified Data.Vector as V import qualified Data.HashMap.Strict as Map+import GHC.Generics import Test.QuickCheck.Arbitrary import Test.QuickCheck+import Test.SmallCheck.Series instance Arbitrary Text where arbitrary = Text.pack <$> sized (`vectorOf` alphabetic)@@ -28,15 +33,18 @@ instance (Arbitrary v) => Arbitrary (Map.HashMap Text v) where arbitrary = makeMap <$> arbitrary- where- makeMap = Map.fromList- . nubBy ((==) `on` fst)- . sortBy (compare `on` fst) +-- | Helper function for generating Arbitrary and Series instances+-- for @Data.HashMap.Strict.Map@ from lists of pairs.+makeMap = Map.fromList+ . nubBy ((==) `on` fst)+ . sortBy (compare `on` fst)+ instance Arbitrary Scientific where arbitrary = scientific <$> arbitrary <*> arbitrary -- TODO: top value has to be complex: Object or Array+-- TODO: how to accumulate cost when generating the series? instance Arbitrary Value where arbitrary = sized arb where@@ -61,5 +69,21 @@ -- | Arbitrary JSON (must start with Object or Array.) arbitraryTopValue :: Gen Value-arbitraryTopValue = sized $ oneof . complexGens+arbitraryTopValue = sized $ oneof . complexGens++-- * SmallCheck Serial instances+instance Monad m => Serial m Text where+ series = newtypeCons Text.pack++instance Monad m => Serial m Scientific where+ series = cons2 scientific++instance Serial m a => Serial m (V.Vector a) where+ series = newtypeCons V.fromList++instance Serial m v => Serial m (Map.HashMap Text v) where+ series = newtypeCons makeMap++-- This one is generated with Generics and instances above+instance Monad m => Serial m Value
Data/Aeson/AutoType/Type.hs view
@@ -6,12 +6,16 @@ Type(..), emptyType, isSimple, isArray, isObject, typeAsSet, hasNonTopTObj,- hasTObj) where+ hasTObj,+ isNullable,+ emptySetLikes) where +import Prelude hiding (any) import qualified Data.HashMap.Strict as Hash import qualified Data.Set as Set import Data.Data (Data(..)) import Data.Typeable (Typeable)+import Data.Foldable (any) import Data.Text (Text) import Data.Set (Set ) import Data.HashMap.Strict(HashMap)@@ -97,33 +101,53 @@ typeSize (TUnion u) = (1+) . sum . (0:) . map typeSize . Set.toList $ u typeSize (TLabel _) = error "Don't know how to compute typeSize of TLabel." +-- | Check if this is nullable (Maybe) type, or not.+-- Nullable type will always accept TNull or missing key that contains it.+isNullable :: Type -> Bool+isNullable TNull = True+isNullable (TUnion u) = isNullable `any` u+isNullable other = False++-- | "Null-ish" types+emptySetLikes :: Set Type+emptySetLikes = Set.fromList [TNull, TArray $ TUnion $ Set.fromList []]+-- Q: and TObj $ Map.fromList []?+{-# INLINE emptySetLikes #-}++-- | Convert any type into union type (even if just singleton). typeAsSet :: Type -> Set Type typeAsSet (TUnion s) = s typeAsSet t = Set.singleton t -hasTObj, hasNonTopTObj, isArray, isUnion, isSimple, isObject :: Type -> Bool -- | Is the top-level constructor a TObj?+isObject :: Type -> Bool isObject (TObj _) = True isObject _ = False -- | Is it a simple (non-compound) Type?+isSimple :: Type -> Bool isSimple x = not (isObject x) && not (isArray x) && not (isUnion x) -- | Is the top-level constructor a TUnion?+isUnion :: Type -> Bool isUnion (TUnion _) = True isUnion _ = False -- | Is the top-level constructor a TArray?+-- | Check if the given type has non-top TObj.+isArray :: Type -> Bool isArray (TArray _) = True isArray _ = False +-- | Check if the given type has non-top TObj.+hasNonTopTObj :: Type -> Bool hasNonTopTObj (TObj o) = any hasTObj $ Hash.elems $ unDict o hasNonTopTObj _ = False +-- | 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) = setAny u- where- setAny = Set.foldr ((||) . hasTObj) False+hasTObj (TUnion u) = any hasTObj u hasTObj _ = False
GenerateTestJSON.hs view
@@ -5,10 +5,12 @@ {-# LANGUAGE StandaloneDeriving #-} {-# LANGUAGE ViewPatterns #-} {-# 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(..))@@ -18,6 +20,7 @@ import Control.Monad (forM_, forM, when) import qualified Data.ByteString.Lazy.Char8 as BSL import qualified Data.HashMap.Strict as Map+import qualified Data.Set as Set import Data.Aeson import Data.Function (on) import Data.List@@ -83,43 +86,81 @@ 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 = fst $ filterM checkDup list `runState` 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 :: IO () generateTestJSONs = do- results <- forM (zip inputFilenames outputFilenames) $ \(inputFilename, outputFilename) -> do- jsonValue :: Value <- generate $ resize flags_size arbitraryTopValue- BSL.writeFile inputFilename $ encode jsonValue- -- Read type from each file- typeForEachFile <- catMaybes <$> mapM extractTypeFromJSONFile [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 "TopLevel" 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 flags_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 flags_autounify- then unifyCandidates uCands splitted- else splitted- myTrace $ "UNIFIED:\n" ++ pretty unified- -- We start by writing module header- writeHaskellModule outputFilename unified- if flags_test- then do- r <- (==ExitSuccess) <$> system (unwords $ ["runghc", outputFilename] ++ inputFilenames)- when r $ mapM_ removeFile [inputFilename, outputFilename]- return r- else- return True+ testValues :: [Value] <- generate $+ resize flags_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 [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 "TopLevel" 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 flags_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 flags_autounify+ then unifyCandidates uCands splitted+ else splitted+ myTrace $ "UNIFIED:\n" ++ pretty unified+ -- We start by writing module header+ writeHaskellModule outputFilename unified+ if flags_test+ then do+ r <- (==ExitSuccess) <$> system (unwords $ ["runghc", outputFilename] ++ inputFilenames)+ when r $ mapM_ removeFile [inputFilename, outputFilename]+ return r+ else+ return True putStrLn $ "Successfully generated " ++ show (length results) ++ " JSON files, out of planned " ++ show flags_count ++ " cases." where
README.md view
@@ -4,7 +4,7 @@ Parser and printer instances are derived using [Aeson](http://hackage.haskell.org/package/aeson). -The program uses union type unification to trim output declarations. The types of same attribute tag and similar attribute set, are automatically unified using recognition by attribute set matching. (This option can be optionally turned off, or a set of unified types may be given explicitly.) `Either` alternatives is used to assure that all `JSON` inputs seen in example input file are handled correctly.+The program uses union type unification to trim output declarations. The types of same attribute tag and similar attribute set, are automatically unified using recognition by attribute set matching. (This option can be optionally turned off, or a set of unified types may be given explicitly.) `:|:` alternatives (similar to `Either`) are used to assure that all `JSON` inputs seen in example input file are handled correctly. I should probably write a short paper to explain the methodology. @@ -92,7 +92,7 @@ ``` data Parameter = Parameter {- parameterParameterValue :: Either Bool (Either Int Text),+ parameterParameterValue :: Bool :|: Int :|: Text, parameterParameterName :: Text }
TestQC.hs view
@@ -3,13 +3,15 @@ main ) where -import Test.QuickCheck---import Test.QuickCheck.Arbitrary-+import Control.Monad import Data.Aeson import Data.Aeson.AutoType.Extract import Data.Aeson.AutoType.Test() -- Arbitrary instance for Value +import Test.QuickCheck+import Test.SmallCheck+--import Test.QuickCheck.Arbitrary+ prop_typeCheck :: Value -> Bool prop_typeCheck v = v `typeCheck` extractType v @@ -23,8 +25,16 @@ prop_valueTypeSizeAndTypeSize :: Value -> Bool prop_valueTypeSizeAndTypeSize v = valueTypeSize v >= typeSize (extractType v) -} +-- | Maximum reasonable depth for quick testing+depth = 5+ main :: IO ()-main = quickCheck prop_typeCheck- {- prop_typeSize,- prop_valueAndValueTypeSize,- prop_valueTypeSizeAndTypeSize]-}+main = do smallCheck depth prop_typeCheck+ quickCheckWith myArgs prop_typeCheck+ {- prop_typeSize,+ prop_valueAndValueTypeSize,+ prop_valueTypeSizeAndTypeSize]-}+ where+ -- 17 - reasonable size for runghc+ --myArgs i = stdArgs { maxSize=i }+ myArgs = stdArgs { maxSize=17, maxSuccess=1000 }
changelog.md view
@@ -1,5 +1,10 @@ Changelog =========+ 0.3 Apr 2015++ * Passed all smallcheck/quickcheck tests.+ * Approaching release candidate.+ 0.2.5.13 Apr 2015 * Correctly handling lone option, not yet union with optionality.
json-autotype.cabal view
@@ -1,6 +1,6 @@ -- Build information for the package. name: json-autotype-version: 0.2.5.13+version: 0.3 synopsis: Automatic type declaration for JSON input data description: Generates datatype declarations with Aeson's "FromJSON" instances from a set of example ".json" files.@@ -137,6 +137,7 @@ pretty >=1.1 && <1.3, process >=1.1 && <1.4, 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,@@ -146,7 +147,8 @@ default-language: Haskell2010 -- test suite with QuickCheck-executable json-autotype-qc-test+test-suite json-autotype-qc-test+ type: exitcode-stdio-1.0 main-is: TestQC.hs other-modules: Data.Aeson.AutoType.Util, Data.Aeson.AutoType.Test,@@ -172,6 +174,7 @@ pretty >=1.1 && <1.3, process >=1.1 && <1.4, 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,