packages feed

json-directory 0.1.0.0 → 0.1.0.1

raw patch · 5 files changed

+221/−31 lines, 5 filesdep +mtldep +processdep ~aesondep ~basePVP: minor bump suggested

API additions: PVP suggests at least a minor version bump

Dependencies added: mtl, process

Dependency ranges changed: aeson, base

API changes (from Hackage documentation)

+ Data.JSON.Directory: IError :: JSONPath -> String -> IResult a
+ Data.JSON.Directory: ISuccess :: a -> IResult a
+ Data.JSON.Directory: Rule :: (FilePath -> Bool) -> (FilePath -> Text) -> (FilePath -> IO (IResult Value)) -> Rule
+ Data.JSON.Directory: [jsonKey] :: Rule -> FilePath -> Text
+ Data.JSON.Directory: [parser] :: Rule -> FilePath -> IO (IResult Value)
+ Data.JSON.Directory: [predicate] :: Rule -> FilePath -> Bool
+ Data.JSON.Directory: data IResult a
+ Data.JSON.Directory: data NoRuleFor
+ Data.JSON.Directory: data Rule
+ Data.JSON.Directory: decodeDirectory' :: (FromJSON a, MonadIO io) => [Rule] -> FilePath -> io (Either String a)
+ Data.JSON.Directory: defaultRules :: [Rule]
+ Data.JSON.Directory: idecodeStrict :: FromJSON a => ByteString -> IResult a
+ Data.JSON.Directory: instance GHC.Exception.Type.Exception Data.JSON.Directory.NoRuleFor
+ Data.JSON.Directory: instance GHC.Show.Show Data.JSON.Directory.NoRuleFor
+ Data.JSON.Directory: jsonRule :: Rule
+ Data.JSON.Directory: textRule :: Rule

Files

CHANGELOG.md view
@@ -1,5 +1,9 @@ # Revision history for json-directory +## 0.1.0.1 -- 2020-06-02++* Add rules engine to specify how to interpret a file+ ## 0.1.0.0 -- 2019-12-17  * Initial version
README.md view
@@ -2,10 +2,15 @@ entries become keys in a map, and the values are sourced from the contets of each entry. +By default+  * Directories are recured into  * Files ending with `.json` are read as JSON values  * Everything else is interpreted as a string +However, these can be modified with rules, that allow for interpreting+any sort of file as JSON.+ The [example](./example) directory in this repository would result in the following JSON value. @@ -22,3 +27,29 @@   } } ```++## `jsondir`++This package also includes an executable for turning directories into JSON+blobs.++```+jsondir [--help] [--rule <SUFFIX> <FILTER> ...]+        [--[no-]default{s,-json,-text}] <ROOT> ...++  Turn a directory structure into a JSON value++ --rule <SUFFIX> <FILTER>     Filter the contents of files with the given+                              SUFFIX with FILTER. The default rules use .json+                              files as is, and treat everything else as strings.+                              Can be specified multiple times. Rule are tried in+                              the order specified.+ --[no-]defaults              Enable or disable the default rules. Default on.+ --[no-]default-json          Enable or disable the .json file rule+ --[no-]default-text          Enable or disable the raw file to JSON string rule.+ <ROOT>                       Directory root to turn into a JSON value++ EXAMPLE+  jsondir --rule '.yml' yaml2json ./my-dir+```+
json-directory.cabal view
@@ -1,7 +1,7 @@ cabal-version:       2.2  name:                json-directory-version:             0.1.0.0+version:             0.1.0.1 category:            Codec synopsis:            Load JSON from files in a directory structure description:         Load JSON from files in a directory structure. The object@@ -22,12 +22,12 @@ library   exposed-modules:     Data.JSON.Directory-  build-depends:       base ^>=4.12.0.0, aeson, directory, filepath, text, unordered-containers+  build-depends:       base ^>=4.12.0.0, aeson, bytestring, directory, filepath, text, unordered-containers   hs-source-dirs:      src   default-language:    Haskell2010  executable jsondir-  build-depends:       base ^>=4.12.0.0, json-directory, bytestring, aeson+  build-depends:       base ^>=4.12.0.0, json-directory, bytestring, aeson, process, filepath, text, mtl   hs-source-dirs:      jsondir   default-language:    Haskell2010   main-is: Main.hs
jsondir/Main.hs view
@@ -3,17 +3,100 @@ {-# LANGUAGE BlockArguments #-} module Main where -import Data.Aeson+import Control.Exception import Control.Monad-import System.Environment+import Control.Monad.Writer+import Control.Monad.Except+import Data.Aeson+import Data.Foldable import Data.JSON.Directory+import Data.List (isSuffixOf)+import Data.Maybe (isNothing)+import System.Environment+import System.Exit+import System.IO+import System.FilePath+import System.Process+import qualified Data.ByteString.Char8 as BS import qualified Data.ByteString.Lazy.Char8 as B+import qualified Data.Text as Text +parseArgs :: Bool -> Bool -> [String] -> ExceptT (Maybe String) (Writer [Rule]) [FilePath]+parseArgs b c ("--rule":rest) = case rest of+    pat:filter:rest -> do+        let+            rule = Rule+                { predicate = isSuffixOf pat+                , jsonKey   = Text.pack . takeBaseName+                , parser    =+                    let+                        parse fp = do+                            s <- readFile fp+                            r <- readCreateProcess (shell filter) s+                            pure $ idecodeStrict (BS.pack r)+                        handleFailure :: IOError -> IO (IResult Value)+                        handleFailure = pure . IError [] . displayException+                    in+                        \fp -> catch (parse fp) handleFailure+                }+        tell [rule]+        parseArgs b c rest+    _ -> throwError $ Just "Invalid argument. Expected `--rule <SUFFIX> <FILTER>`"+parseArgs b c ("--no-default-json":rest) = parseArgs False c rest+parseArgs b c ("--default-json":rest) = parseArgs True c rest+parseArgs b c ("--no-default-text":rest) = parseArgs b False rest+parseArgs b c ("--default-text":rest) = parseArgs b True rest+parseArgs b c ("--no-defaults":rest) = parseArgs False False rest+parseArgs _ _ ("--help":_) = throwError Nothing+parseArgs b c ("--":rest) = do+    when b (tell [jsonRule])+    when c (tell [textRule])+    pure rest+parseArgs b c (a:as) = (a:) <$> parseArgs b c as+parseArgs b c [] = do+    when b (tell [jsonRule])+    when c (tell [textRule])+    pure []++usage :: String+usage = "\+\jsondir [--help] [--rule <SUFFIX> <FILTER> ...]\n\+\        [--[no-]default{s,-json,-text}] <ROOT> ...\n\+\\n\+\  Turn a directory structure into a JSON value\n\+\\n\+\ --rule <SUFFIX> <FILTER>     Filter the contents of files with the given\n\+\                              SUFFIX with FILTER. The default rules use .json\n\+\                              files as is, and treat everything else as strings.\n\+\                              Can be specified multiple times. Rule are tried in\n\+\                              the order specified.\n\+\ --[no-]defaults              Enable or disable the default rules. Default on.\n\+\ --[no-]default-json          Enable or disable the .json file rule\n\+\ --[no-]default-text          Enable or disable the raw file to JSON string rule.\n\+\ <ROOT>                       Directory root to turn into a JSON value\n\+\\n\+\ EXAMPLE\n\+\  jsondir --rule '.yml' yaml2json ./my-dir\n\+\"+ main :: IO () main = do-    as <- getArgs-    forM_ as \a -> do-        decodeDirectory @Value a >>= \case-            Left e -> error e-            Right v -> B.putStrLn $ encode v+    (eRoots, rules) <- runWriter . runExceptT . parseArgs True True <$> getArgs++    case eRoots of+        Left err -> do+            traverse_ (hPutStrLn stderr) err+            if isNothing err+            then putStrLn usage+            else do+                hPutStrLn stderr "Usage:"+                hPutStrLn stderr usage+            exitFailure+        Right roots -> do+            forM_ roots \a -> do+                decodeDirectory' @Value rules a >>= \case+                    Left e -> do+                        hPutStrLn stderr $ "Error: " ++ e+                        exitFailure+                    Right v -> B.putStrLn $ encode v 
src/Data/JSON/Directory.hs view
@@ -3,13 +3,25 @@ {-# LANGUAGE BlockArguments #-} module Data.JSON.Directory     ( decodeDirectory+    , decodeDirectory'+    , Rule(..)+    , IResult(..)+    , defaultRules+    , jsonRule+    , textRule+    , idecodeStrict     , ModifiedWhileReading+    , NoRuleFor     ) where  import Control.Exception import Control.Monad import Control.Monad.IO.Class import Data.Aeson+import Data.Aeson.Internal (IResult(..), formatError, ifromJSON)+import Data.Aeson.Parser.Internal (eitherDecodeStrictWith, jsonEOF)+import Data.Aeson.Types+import qualified Data.ByteString as BS import Data.HashMap.Strict import Data.List import Data.Maybe@@ -19,26 +31,65 @@ import System.Directory import System.FilePath +-- Exception is thrown if the files changed while we were+-- reading them. data ModifiedWhileReading = ModifiedWhileReading FilePath     deriving (Show)  instance Exception ModifiedWhileReading +-- Exception thrown if no rule was specified for a given file.+data NoRuleFor = NoRuleFor FilePath+    deriving Show++instance Exception NoRuleFor++-- | How to interpret a file.+data Rule = Rule+    { predicate :: FilePath -> Bool+        -- ^ A predicate to see if this rule applies.+    , jsonKey   :: FilePath -> Text+        -- ^ A function to transform the filename into a JSON key value+    , parser    :: FilePath -> IO (IResult Value)+        -- ^ Turn a file into a Value.  The @JSONPath@ in the @IResult@ will be+        -- merged into the correct location.+    }++-- | A rule that reads @.json@ files as JSON.+jsonRule :: Rule+jsonRule = Rule+    { predicate = isSuffixOf ".json"+    , jsonKey   = Text.pack . takeBaseName+    , parser    = idecodeFileStrict+    }++-- | A rule that reads any file into a JSON string.+textRule :: Rule+textRule = Rule+    { predicate = const True+    , jsonKey   = Text.pack . takeFileName+    , parser    = fmap (ISuccess . String) . Text.readFile+    }++-- | Some sane default rules. Attempts do do @`jsonRule`@ and falls back to+-- @`textRule`@+defaultRules :: [Rule]+defaultRules = [jsonRule, textRule]+ data EntryType     = Directory-    | JSON-    | TextFile+    | File (FilePath -> IO (IResult Value)) -pathType :: FilePath -> IO (Text, EntryType)-pathType p = do+pathType :: [Rule] -> FilePath -> IO (Text, EntryType)+pathType rules p = do     doesDirectoryExist p >>= \case         True -> pure (Text.pack $ takeFileName p, Directory)-        False -> pure case splitExtension (takeFileName p) of-            (name, ".json") -> (Text.pack $ name, JSON)-            _               -> (Text.pack $ takeFileName p, TextFile)+        False -> case find (\r -> predicate r p) rules of+            Nothing   -> throwIO $ NoRuleFor p+            Just rule -> pure (jsonKey rule p, File (parser rule)) -decodeDirectoryValue :: MonadIO io => FilePath -> io (Either String Value)-decodeDirectoryValue path = liftIO $ do+decodeDirectoryValue :: MonadIO io => [Rule] -> FilePath -> io (IResult Value)+decodeDirectoryValue rules path = liftIO $ do     time <- getModificationTime path     ents <- listDirectory path     kvs <- catMaybes <$> forM ents \ent -> do@@ -46,19 +97,34 @@         then pure Nothing         else Just <$> do             let path' = path </> ent-            pathType path' >>= \case-                (n, Directory) -> (n,) <$> decodeDirectoryValue path'-                (n, JSON     ) -> (n,) <$> eitherDecodeFileStrict path'-                (n, TextFile ) -> (n,) . Right . String <$> Text.readFile path'+            pathType rules path' >>= \case+                (n, Directory) -> (n,) . addContext n <$> decodeDirectoryValue rules path'+                (n, File parser) -> (n,) . addContext n <$> parser path'     time2 <- getModificationTime path     unless (time == time2) $ throwIO (ModifiedWhileReading path)-     pure $ Object <$> sequence (Data.HashMap.Strict.fromList kvs) -resultToEither :: Result a -> Either String a-resultToEither (Success a) = Right a-resultToEither (Error s)   = Left s+addContext :: Text -> IResult a -> IResult a+addContext c (IError p s) = IError (Key c : p) s+addContext _ x = x +idecodeFileStrict :: (FromJSON a) => FilePath -> IO (IResult a)+idecodeFileStrict =+    fmap (toIResult . eitherDecodeStrictWith jsonEOF ifromJSON) . BS.readFile+  where+    toIResult (Left (p, s)) = IError p s+    toIResult (Right a) = ISuccess a++idecodeStrict :: (FromJSON a) => BS.ByteString -> IResult a+idecodeStrict = toIResult . eitherDecodeStrictWith jsonEOF ifromJSON+  where+    toIResult (Left (p, s)) = IError p s+    toIResult (Right a) = ISuccess a++resultToEither :: IResult a -> Either String a+resultToEither (ISuccess a) = Right a+resultToEither (IError p s) = Left $ formatError p s+ -- | Takes a directory and decodes it using a @`FromJSON`@ instance. -- Each entry in the directory becomes a key, and the contents become -- the corresponding value.@@ -69,9 +135,15 @@ -- -- This function can throw IO exceptions as well as a @`ModifiedWhileReading`@ -- exception if the modification time changes during processing.+--+-- Uses @`defaultRules`@ decodeDirectory :: (FromJSON a, MonadIO io) => FilePath -> io (Either String a)-decodeDirectory p = do-    ev <- decodeDirectoryValue p-    pure $ do+decodeDirectory = decodeDirectory' defaultRules++-- | Like @`decodeDirectory`@ but you get to specify the rules.+decodeDirectory' :: (FromJSON a, MonadIO io) => [Rule] -> FilePath -> io (Either String a)+decodeDirectory' rules p = do+    ev <- decodeDirectoryValue rules p+    pure . resultToEither $ do         v <- ev-        resultToEither $ fromJSON v+        ifromJSON v