packages feed

gitson 0.2.1 → 0.3.0

raw patch · 5 files changed

+162/−47 lines, 5 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Gitson.Util: devNull :: IO Handle
+ Gitson: readEntryById :: FromJSON a => FilePath -> Int -> IO (Maybe a)
+ Gitson: readEntryByName :: FromJSON a => FilePath -> String -> IO (Maybe a)
+ Gitson: saveNextEntry :: ToJSON a => FilePath -> String -> a -> TransactionWriter
+ Gitson.Util: intoMaybe :: Maybe a -> b -> Maybe (a, b)
+ Gitson.Util: maybeReadIntString :: String -> Maybe (Int, String)
+ Gitson.Util: nextKeyId :: [String] -> Int
- Gitson: listEntryKeys :: FilePath -> IO [FilePath]
+ Gitson: listEntryKeys :: FilePath -> IO [String]
- Gitson: readEntry :: FromJSON a => FilePath -> FilePath -> IO (Maybe a)
+ Gitson: readEntry :: FromJSON a => FilePath -> String -> IO (Maybe a)
- Gitson: saveEntry :: ToJSON a => FilePath -> FilePath -> a -> TransactionWriter
+ Gitson: saveEntry :: ToJSON a => FilePath -> String -> a -> TransactionWriter
- Gitson.Util: entryPath :: FilePath -> FilePath -> FilePath
+ Gitson.Util: entryPath :: FilePath -> String -> FilePath
- Gitson.Util: filterFilenamesAsKeys :: [String] -> [String]
+ Gitson.Util: filterFilenamesAsKeys :: [FilePath] -> [String]

Files

README.md view
@@ -32,7 +32,7 @@   -- Also, transactions are thread-safe   transaction "./content" $ do     -- order: (collection) (key        ) (data)-    saveEntry "content"    "first-thing" Thing {val = 1}+    saveEntry "things"     "first-thing" Thing {val = 1}     -- Collections are created automatically, like in MongoDB     liftIO $ putStrLn "Written first-thing"     -- You have to use liftIO to do IO actions inside of a transaction!@@ -41,7 +41,9 @@   -- Reading data   -- (These are normal IO actions, so if you want   --  to read inside of a transaction, liftIO.-  --  Note: transaction already includes insideDirectory!)+  --  Note: transaction already includes insideDirectory!+  --  Warning: you can't read what you're writing in a transaction!!!+  --  You can only read what's been written before the transaction began.)   insideDirectory "./content" $ do     colls <- listCollections           -- ["things"]@@ -56,6 +58,17 @@   -- the current directory, executes an action and changes it back.   -- You can use reading actions without it, like this:   keys <- listEntryKeys "./content/things"+++  -- And now, some bells and whistles:+  -- Numeric id support+  transaction "./content" $ do+    saveNextEntry "things" "hello-world" Thing {val = 1}+    -- will save to things/000001-hello-world.json+  insideDirectory "./content" $ do+    thing <- readEntryById "things" 1+    same-thing <- readEntryByName "thing" "hello-world"+    -- both will read from things/000001-hello-world.json ```  ## Development
gitson.cabal view
@@ -1,5 +1,5 @@ name:            gitson-version:         0.2.1+version:         0.3.0 synopsis:        A document store library for Git + JSON. description:     A simple document store library for Git + JSON, based on Aeson. Uses command line git, at least for now. No fancy indexes and stuff, but it does what I need right now. Transactions use flock, so it's safe even across completely separate programs! category:        Database, JSON, Git
library/Gitson.hs view
@@ -4,22 +4,25 @@   createRepo,   transaction,   saveEntry,-  readEntry,+  saveNextEntry,+  listCollections,   listEntryKeys,   listEntries,-  listCollections+  readEntry,+  readEntryById,+  readEntryByName ) where  import           System.Directory import           System.Lock.FLock-import           Control.Exception-import           Control.Applicative-import           Control.Error.Util+import           Control.Exception (try, IOException)+import           Control.Error.Util (hush) import           Control.Monad.Trans.Writer-import           Control.Monad.IO.Class (liftIO) import           Data.Aeson (ToJSON, FromJSON, decode) import           Data.Aeson.Encode.Pretty-import           Data.Maybe (fromMaybe)+import           Data.Maybe (fromMaybe, catMaybes)+import           Data.List (find, isSuffixOf)+import           Text.Printf (printf) import qualified Data.ByteString.Lazy as BL import           Gitson.Util @@ -38,31 +41,49 @@ transaction :: FilePath -> TransactionWriter -> IO () transaction repoPath action = do   insideDirectory repoPath $ withLock lockPath Exclusive Block $ do-    shell "git" ["reset", "--hard", "HEAD"] -- Reset working tree just in case     writeActions <- execWriterT action+    shell "git" ["stash"] -- it's totally ok to do this without changes     sequence_ writeActions     shell "git" ["add", "--all"]     shell "git" ["commit", "-m", "Gitson transaction"]+    shell "git" ["stash", "pop"] +combineKey :: (Int, String) -> String+combineKey (n, s) = zeroPad n ++ "-" ++ s+  where zeroPad :: Int -> String+        zeroPad x = printf "%06d" x+ prettyConfig :: Config prettyConfig = Config { confIndent = 2, confCompare = compare } +writeEntry :: ToJSON a => FilePath -> String -> a -> IO ()+writeEntry collection key content = BL.writeFile (entryPath collection key) (encodePretty' prettyConfig content)+ -- | Adds a write action to a transaction.-saveEntry :: ToJSON a => FilePath -> FilePath -> a -> TransactionWriter+saveEntry :: ToJSON a => FilePath -> String -> a -> TransactionWriter saveEntry collection key content = do-  liftIO $ createDirectoryIfMissing True collection-  tell [BL.writeFile (entryPath collection key) (encodePretty' prettyConfig content)]+  tell [createDirectoryIfMissing True collection,+        writeEntry collection key content] --- | Reads an entry from a collection by key.-readEntry :: FromJSON a => FilePath -> FilePath -> IO (Maybe a)-readEntry collection key = do-  jsonString <- try (BL.readFile $ entryPath collection key) :: IO (Either IOException BL.ByteString)-  return $ decode =<< hush jsonString+-- | Adds a write action to a transaction.+-- The key will start with a numeric id, incremented from the last id in the collection.+saveNextEntry :: ToJSON a => FilePath -> String -> a -> TransactionWriter+saveNextEntry collection key content = do+  tell [createDirectoryIfMissing True collection,+        listEntryKeys collection >>=+        return . nextKeyId >>=+        \nextId -> writeEntry collection (combineKey (nextId, key)) content] +-- | Lists collections in the current repository.+listCollections :: IO [FilePath]+listCollections = do+  contents <- try (getDirectoryContents =<< getCurrentDirectory) :: IO (Either IOException [FilePath])+  filterDirs $ fromMaybe [] $ hush contents+ -- | Lists entry keys in a collection.-listEntryKeys :: FilePath -> IO [FilePath]+listEntryKeys :: FilePath -> IO [String] listEntryKeys collection = do-  contents <- try (getDirectoryContents collection) :: IO (Either IOException [FilePath])+  contents <- try (getDirectoryContents collection) :: IO (Either IOException [String])   return $ filterFilenamesAsKeys $ fromMaybe [] $ hush contents  -- | Lists entries in a collection.@@ -72,8 +93,22 @@   maybes <- mapM (readEntry collection) ks   return $ fromMaybe [] $ sequence maybes --- | Lists collections in the current repository.-listCollections :: IO [FilePath]-listCollections = do-  contents <- try (getDirectoryContents =<< getCurrentDirectory) :: IO (Either IOException [FilePath])-  filterDirs $ fromMaybe [] $ hush contents+-- | Reads an entry from a collection by key.+readEntry :: FromJSON a => FilePath -> String -> IO (Maybe a)+readEntry collection key = do+  jsonString <- try (BL.readFile $ entryPath collection key) :: IO (Either IOException BL.ByteString)+  return $ decode =<< hush jsonString++splitFindAndReadEntry :: FromJSON a => FilePath -> ([((Int, String), String)] -> Maybe ((Int, String), String)) -> IO (Maybe a)+splitFindAndReadEntry collection finder = listEntryKeys collection >>=+  maybeReadEntry . finder . catMaybes . (map $ \x -> intoMaybe (maybeReadIntString x) x)+  where maybeReadEntry (Just x) = readEntry collection $ snd x+        maybeReadEntry Nothing = return Nothing++-- | Reads an entry from a collection by numeric id (for example, key "00001-hello" has id 1)..+readEntryById :: FromJSON a => FilePath -> Int -> IO (Maybe a)+readEntryById collection n = splitFindAndReadEntry collection $ find ((== n) . fst . fst)++-- | Reads an entry from a collection by name (for example, key "00001-hello" has name "hello").+readEntryByName :: FromJSON a => FilePath -> String -> IO (Maybe a)+readEntryByName collection n = splitFindAndReadEntry collection $ find ((isSuffixOf n) . snd . fst)
library/Gitson/Util.hs view
@@ -2,7 +2,9 @@ module Gitson.Util (module Gitson.Util) where  import           Control.Monad (void, filterM)+import           Control.Applicative import           Data.List (isSuffixOf, isPrefixOf)+import           Data.Maybe import           System.FilePath import           System.Directory import           System.Process@@ -15,7 +17,7 @@ -- -- >>> entryPath "things/" "entry" -- "things/entry.json"-entryPath :: FilePath -> FilePath -> FilePath+entryPath :: FilePath -> String -> FilePath entryPath collection key = collection </> key <.> "json"  -- | Path to the transaction lock file, relative to the repo root.@@ -26,7 +28,7 @@ -- -- >>> filterFilenamesAsKeys [".", "..", "k1.json", "unrelated.file"] -- ["k1"]-filterFilenamesAsKeys :: [String] -> [String]+filterFilenamesAsKeys :: [FilePath] -> [String] filterFilenamesAsKeys = map dropExtension . filter (isSuffixOf ".json")  -- | Filters a list of file paths, leaving only paths to existing non-hidden directories.@@ -47,13 +49,44 @@ lastCommitText :: IO String lastCommitText = readProcess "git" ["log", "--max-count=1", "--pretty=format:%s"] [] --- | A \/dev/null handle.-devNull :: IO Handle-devNull = openFile "/dev/null" ReadWriteMode- -- | Runs a shell command with stdin, stdout and stderr set to /dev/null. shell :: String -> [String] -> IO () shell cmd args = void $ do-  dnull <- devNull+  dnull <- openFile "/dev/null" ReadWriteMode   (_, _, _, pid) <- createProcess (proc cmd args){std_in = UseHandle dnull, std_out = UseHandle dnull, std_err = UseHandle dnull}   waitForProcess pid++-- I can't believe there's nothing like this in Data.Maybe+-- | Adds a value to a Maybe.+intoMaybe :: Maybe a -> b -> Maybe (a, b)+intoMaybe (Just x) y = Just (x, y)+intoMaybe Nothing y = Nothing++-- | Tries to extract the first int out of a string.+--+-- >>> maybeReadIntString "0123-hell0w0rld"+-- Just (123,"-hell0w0rld")+--+-- >>> maybeReadIntString "1"+-- Just (1,"")+--+-- >>> maybeReadIntString "hello"+-- Nothing+maybeReadIntString :: String -> Maybe (Int, String)+maybeReadIntString x = listToMaybe (reads x :: [(Int, String)])++-- | Returns the next numeric id in a sequence of keys.+--+-- >>> nextKeyId []+-- 1+--+-- >>> nextKeyId ["aaaaarrrrrrrrrrr"]+-- 1+--+-- >>> nextKeyId ["1-hell0-w0rld-123456.json", "002-my-second-post.json"]+-- 3+nextKeyId :: [String] -> Int+nextKeyId = (+1) . maxOrZero . catMaybes . map maybeReadInt+  where maybeReadInt x = fst <$> maybeReadIntString x+        maxOrZero [] = 0+        maxOrZero xs = maximum xs
test-suite/GitsonSpec.hs view
@@ -17,21 +17,33 @@  spec :: Spec spec = before setup $ after cleanup $ do-  describe "transaction" $ do-    it "saves entries only after it's done" $ do-      transaction "tmp/repo" $ do-        saveEntry "things" "first-thing" Thing {val = 1}-        saveEntry "things" "second-thing" Thing {val = 2}-        liftIO $ (readFile "things/first-thing.json") `shouldThrow` anyIOException-        liftIO $ (readFile "things/second-thing.json") `shouldThrow` anyIOException-      insideDirectory "tmp/repo" $ do-        first <- readFile "things/first-thing.json"-        first `shouldBe` "{\n  \"val\": 1\n}"-        second <- readFile "things/second-thing.json"-        second `shouldBe` "{\n  \"val\": 2\n}"-        commitMsg <- lastCommitText-        commitMsg `shouldBe` "Gitson transaction"+  context "transaction" $ do+    describe "saveEntry" $ do+      it "saves entries only after it's done" $ do+        transaction "tmp/repo" $ do+          saveEntry "things" "first-thing" Thing {val = 1}+          saveEntry "things" "second-thing" Thing {val = 2}+          liftIO $ (readFile "things/first-thing.json") `shouldThrow` anyIOException+          liftIO $ (readFile "things/second-thing.json") `shouldThrow` anyIOException+        insideDirectory "tmp/repo" $ do+          first <- readFile "things/first-thing.json"+          first `shouldBe` "{\n  \"val\": 1\n}"+          second <- readFile "things/second-thing.json"+          second `shouldBe` "{\n  \"val\": 2\n}"+          commitMsg <- lastCommitText+          commitMsg `shouldBe` "Gitson transaction" +    describe "saveNextEntry" $ do+      it "saves entries with next numeric ids" $ do+        transaction "tmp/repo" $ do+          saveNextEntry "things" "hello" Thing {val = 1}+          saveNextEntry "things" "world" Thing {val = 2}+        insideDirectory "tmp/repo" $ do+          first <- readFile "things/000001-hello.json"+          first `shouldBe` "{\n  \"val\": 1\n}"+          second <- readFile "things/000002-world.json"+          second `shouldBe` "{\n  \"val\": 2\n}"+   describe "readEntry" $ do     it "returns Just the entry when reading an entry by key" $ do       createDirectoryIfMissing True "tmp/repo/things"@@ -41,6 +53,28 @@      it "returns Nothing when reading by a nonexistent key" $ do       content <- readEntry "tmp/repo/things" "totally-not-a-thing" :: IO (Maybe Thing)+      content `shouldBe` Nothing++  describe "readEntryById" $ do+    it "returns Just the entry when reading an entry by id" $ do+      createDirectoryIfMissing True "tmp/repo/things"+      _ <- writeFile "tmp/repo/things/000004-second-thing.json" "{\"val\":1}"+      content <- readEntryById "tmp/repo/things" 4 :: IO (Maybe Thing)+      content `shouldBe` Just Thing {val = 1}++    it "returns Nothing when reading by a nonexistent id" $ do+      content <- readEntryById "tmp/repo/things" 1 :: IO (Maybe Thing)+      content `shouldBe` Nothing++  describe "readEntryByName" $ do+    it "returns Just the entry when reading an entry by name" $ do+      createDirectoryIfMissing True "tmp/repo/things"+      _ <- writeFile "tmp/repo/things/000098-second-thing.json" "{\"val\":1}"+      content <- readEntryByName "tmp/repo/things" "second-thing" :: IO (Maybe Thing)+      content `shouldBe` Just Thing {val = 1}++    it "returns Nothing when reading by a nonexistent name" $ do+      content <- readEntryByName "tmp/repo/things" "yolo" :: IO (Maybe Thing)       content `shouldBe` Nothing    describe "listEntryKeys" $ do