packages feed

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 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,