packages feed

couchdb-conduit 0.1.0.1 → 0.1.1.0

raw patch · 7 files changed

+196/−145 lines, 7 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- Database.CouchDB.Conduit.View: couchViewPut :: MonadCouch m => Path -> Path -> ByteString -> ByteString -> Maybe ByteString -> ResourceT m Revision
- Database.CouchDB.Conduit.View: instance Data CouchDesignDoc
- Database.CouchDB.Conduit.View: instance Default CouchDesignDoc
- Database.CouchDB.Conduit.View: instance Eq CouchDesignDoc
- Database.CouchDB.Conduit.View: instance Show CouchDesignDoc
- Database.CouchDB.Conduit.View: instance Typeable CouchDesignDoc
+ Database.CouchDB.Conduit.Design: couchViewPut :: MonadCouch m => Path -> Path -> ByteString -> Maybe ByteString -> ResourceT m Revision

Files

couchdb-conduit.cabal view
@@ -1,12 +1,12 @@ name:           couchdb-conduit-version:        0.1.0.1
+version:        0.1.1.0
 cabal-version:  >= 1.8 build-type:     Simple stability:      Experimental category:       Database, Conduit license:        BSD3 license-file:   LICENSE-author:         John Lenz <lenz@math.uic.edu>, Alexander Dorofeev <aka.spin@gmail.com>+author:         Alexander Dorofeev <aka.spin@gmail.com>, John Lenz <lenz@math.uic.edu>
 maintainer:     Alexander Dorofeev <aka.spin@gmail.com> synopsis:       Couch DB client library using http-conduit and aeson homepage:       https://github.com/akaspin/couchdb-conduit@@ -17,7 +17,7 @@     attoparsec fit togther so well that this package is mostly just a direct combination     of these packages.  The single additional feature in this package is an attoparsec parser     for views, which allows constant memory processing of view returns.-data-files:     test/Database/CouchDB/Conduit/Test/Explicit.hs, test/Database/CouchDB/Conduit/Test/Generic.hs, test/Database/CouchDB/Conduit/Test/Util.hs, test/Database/CouchDB/Conduit/Test/View.hs, test/Main.hs
+data-files:     test/Database/CouchDB/Conduit/Test/Explicit.hs, test/Database/CouchDB/Conduit/Test/Generic.hs, test/Database/CouchDB/Conduit/Test/Util.hs, test/Database/CouchDB/Conduit/Test/View.hs, test/Main.hs  source-repository head   type:         git@@ -46,17 +46,18 @@                    data-default,                    blaze-builder   ghc-options:      -Wall-  exposed-modules:  -                    Database.CouchDB.Conduit,-                    Database.CouchDB.Conduit.DB,-                    Database.CouchDB.Conduit.Explicit,-                    Database.CouchDB.Conduit.Generic,-                    Database.CouchDB.Conduit.LowLevel,-                    Database.CouchDB.Conduit.View-  other-modules:    -                    Database.CouchDB.Conduit.Internal.Doc,-                    Database.CouchDB.Conduit.Internal.Parser,-                    Database.CouchDB.Conduit.Internal.View+  exposed-modules:  
+                    Database.CouchDB.Conduit,
+                    Database.CouchDB.Conduit.DB,
+                    Database.CouchDB.Conduit.Design,
+                    Database.CouchDB.Conduit.Explicit,
+                    Database.CouchDB.Conduit.Generic,
+                    Database.CouchDB.Conduit.LowLevel,
+                    Database.CouchDB.Conduit.View
+  other-modules:    
+                    Database.CouchDB.Conduit.Internal.Doc,
+                    Database.CouchDB.Conduit.Internal.Parser,
+                    Database.CouchDB.Conduit.Internal.View
  test-suite test   type:            exitcode-stdio-1.0@@ -86,12 +87,12 @@                    syb,                    containers,                    blaze-builder-  ghc-options:     -Wall -rtsopts -threaded
+  ghc-options:     -Wall -rtsopts -threaded   hs-source-dirs:  test   main-is:         Main.hs-  other-modules:   -                   Database.CouchDB.Conduit.Test.Explicit,-                   Database.CouchDB.Conduit.Test.Util,-                   Database.CouchDB.Conduit.Test.View,-                   Database.CouchDB.Conduit.Test.Generic+  other-modules:   
+                   Database.CouchDB.Conduit.Test.Explicit,
+                   Database.CouchDB.Conduit.Test.Util,
+                   Database.CouchDB.Conduit.Test.View,
+                   Database.CouchDB.Conduit.Test.Generic
 
+ src/Database/CouchDB/Conduit/Design.hs view
@@ -0,0 +1,63 @@+{-# LANGUAGE OverloadedStrings, ScopedTypeVariables #-} 
+
+-- | High-level API for CouchDB design documents. These methods are very 
+--   convenient for bootstrapping and testing.
+
+module Database.CouchDB.Conduit.Design (
+    couchViewPut
+) where
+
+import              Prelude hiding (catch)
+import              Control.Exception.Lifted (catch)
+
+import              Data.Conduit (ResourceT)
+
+import qualified    Data.ByteString as B
+import qualified    Data.Text as T
+import qualified    Data.Text.Encoding as TE
+import qualified    Data.HashMap.Lazy as M
+import qualified    Data.Aeson as A
+import qualified    Data.Aeson.Types as AT
+
+import Database.CouchDB.Conduit (MonadCouch, CouchError, Path, mkPath, Revision)
+import Database.CouchDB.Conduit.Internal.Doc (couchGetWith, couchPutWith)
+
+-- | Put view in design document. If design document does not exist, it 
+--   will be created. 
+couchViewPut :: MonadCouch m =>
+       Path                 -- ^ Design document
+    -> Path                 -- ^ View name
+    -> B.ByteString         -- ^ Map function
+    -> Maybe B.ByteString   -- ^ Reduce function
+    -> ResourceT m Revision
+couchViewPut designName viewName mapF reduceF = do
+    -- Get design or empty object
+    (rev, A.Object d) <- catch 
+        (couchGetWith (A.Success) path [])
+        (\(_ :: CouchError) -> return (B.empty, AT.emptyObject))
+    couchPutWith A.encode path rev [] $ inferViews (purge_ d)
+  where
+    path = (mkPath ["_design", designName])
+    inferViews d = A.Object $ M.insert "views" (addView d) d
+    addView d = A.Object $ M.insert 
+        (TE.decodeUtf8 viewName)
+        (constructView mapF reduceF) 
+        (extractViews d)
+    constructView :: B.ByteString -> Maybe B.ByteString -> A.Value
+    constructView m (Just r) = A.object ["map" A..= m, "reduce" A..= r]
+    constructView m Nothing = A.object ["map" A..= m]
+
+-- | Purge underscore fields
+purge_ :: AT.Object -> AT.Object
+purge_ = M.filterWithKey (\k _ -> not $ k `elem` ["_id", "_rev"])
+
+-- | Strip 'A.Value'
+stripObject :: AT.Value -> AT.Object
+stripObject (A.Object a) = a 
+stripObject _ = M.empty 
+
+-- Extract views field or return empty map
+extractViews :: M.HashMap T.Text AT.Value -> M.HashMap T.Text AT.Value
+extractViews o = maybe M.empty stripObject $ M.lookup "views" o
+
+
src/Database/CouchDB/Conduit/Internal/Doc.hs view
@@ -4,11 +4,12 @@ module Database.CouchDB.Conduit.Internal.Doc (
     couchRev,
     couchDelete,
-    couchGetRaw,
     
     couchGetWith,
     couchPutWith,
-    couchPutWith'
+    couchPutWith',
+    
+    couchGetRaw
 ) where
 
 import              Prelude hiding (catch)
@@ -25,7 +26,8 @@ import              Network.HTTP.Types as HT
 
 import              Database.CouchDB.Conduit
-import              Database.CouchDB.Conduit.LowLevel (couch, protect')
+import              Database.CouchDB.Conduit.LowLevel 
+                        (couch, protect')
 import              Database.CouchDB.Conduit.Internal.Parser
 
 ------------------------------------------------------------------------------
@@ -55,20 +57,6 @@                protect' >> return ()
                
 ------------------------------------------------------------------------------
--- low-level 
-------------------------------------------------------------------------------
-
--- | Load raw 'A.Value' from single object from couch DB.
-couchGetRaw :: MonadCouch m => 
-       Path         -- ^ Document path
-    -> HT.Query     -- ^ Query
-    -> ResourceT m A.Value
-couchGetRaw p q = do
-    H.Response _ _ bsrc <- couch HT.methodGet p [] q 
-            (H.RequestBodyBS B.empty) protect'
-    bsrc $$ CA.sinkParser A.json
-    
-------------------------------------------------------------------------------
 -- with converter
 ------------------------------------------------------------------------------
 
@@ -82,7 +70,7 @@     H.Response _ _ bsrc <- couch HT.methodGet p [] q 
             (H.RequestBodyBS B.empty) protect'
     j <- bsrc $$ CA.sinkParser A.json
-    A.String r <- lift $ extractField "_rev" j
+    A.String r <- lift $ either resourceThrow return $ extractField "_rev" j
     o <- lift $ jsonToTypeWith f j 
     return (TE.encodeUtf8 r, o)
 
@@ -99,7 +87,7 @@     H.Response _ _ bsrc <- couch HT.methodPut p (ifMatch r) q 
             (H.RequestBodyLBS $ f val) protect'
     j <- bsrc $$ CA.sinkParser A.json
-    lift $ extractRev j
+    lift $ either resourceThrow return $ extractRev j
   where 
     ifMatch "" = []
     ifMatch rv = [("If-Match", rv)]
@@ -118,3 +106,15 @@     handler404 (CouchError (Just 404) _) = return ""
     handler404 e = lift $ resourceThrow e
 
+
+-- | Load raw 'A.Value' from single object from couch DB.
+couchGetRaw :: MonadCouch m => 
+       Path         -- ^ Document path
+    -> HT.Query     -- ^ Query
+    -> ResourceT m (Revision, A.Value)
+couchGetRaw p q = do
+    H.Response _ _ bsrc <- couch HT.methodGet p [] q 
+            (H.RequestBodyBS B.empty) protect'
+    j <- bsrc $$ CA.sinkParser A.json
+    A.String r <- lift $ either resourceThrow return $ extractField "_rev" j
+    return (TE.encodeUtf8 r, j)
src/Database/CouchDB/Conduit/Internal/Parser.hs view
@@ -17,20 +17,19 @@ --    _  -> Left $ CouchError Nothing "unable to find revision"  
 --valToRev _ = Left $ CouchError Nothing "Couch DB did not return an object"
 
-extractRev :: ResourceIO m => A.Value -> m Revision
-extractRev o = do
-    A.String r <- extractField "rev" o
-    return $ TE.encodeUtf8 r
+extractField :: T.Text -> A.Value -> Either CouchError A.Value
+extractField s (A.Object o) = 
+    maybe (Left $ CouchError Nothing $ 
+            "unable to find field " ++ (BU8.toString . TE.encodeUtf8 $ s))
+          Right 
+          $ M.lookup s o
+extractField _ _ = Left $ CouchError Nothing "Couch DB did not return an object"
 
--- | Extract single field
-extractField :: ResourceIO m =>-           T.Text -> A.Value -> m A.Value
-extractField s (A.Object o) = case M.lookup s o of
-    (Just r) -> return r
-    _  -> resourceThrow $ CouchError Nothing $
-            "unable to find field" ++ (BU8.toString . TE.encodeUtf8 $ s)  
-extractField _ _ = resourceThrow $ CouchError Nothing 
-        "Couch DB did not return an object"
+extractRev :: A.Value -> Either CouchError Revision
+extractRev = look . extractField "rev"
+  where 
+    look (Right (A.String a)) = Right $ TE.encodeUtf8 a
+    look _ = Left $ CouchError Nothing "CouchDB object has't revision"
 
 -- | Convert to type with given convertor
 jsonToTypeWith :: ResourceIO m =>
src/Database/CouchDB/Conduit/LowLevel.hs view
@@ -19,7 +19,7 @@ 
 import              Data.Maybe (fromJust)
 import qualified    Data.ByteString as B
-import              Data.Aeson (json, Value(..))
+import qualified    Data.Aeson as A
 import qualified    Data.ByteString.UTF8 as BU8
 import qualified    Data.HashMap.Lazy as M
 import qualified    Data.Text as T
@@ -79,13 +79,13 @@ protect goodCodes ~resp@(H.Response (HT.Status sc sm) _ bsrc)  
     | sc `elem` goodCodes = return resp
     | otherwise = do
-        v <- catch (bsrc $$ sinkParser json)
-                   (\(_::SomeException) -> return Null)
+        v <- catch (bsrc $$ sinkParser A.json)
+                   (\(_::SomeException) -> return A.Null)
         liftBase $ resourceThrow $ CouchError (Just sc) $ msg v
         where 
         msg v = BU8.toString sm ++ reason v
-        reason (Object v) = case M.lookup "reason" v of
-                Just (String t) -> ": " ++ T.unpack t
+        reason (A.Object v) = case M.lookup "reason" v of
+                Just (A.String t) -> ": " ++ T.unpack t
                 _                 -> ""
         reason _ = []
 
src/Database/CouchDB/Conduit/View.hs view
@@ -2,6 +2,8 @@ {-# LANGUAGE DeriveDataTypeable, ScopedTypeVariables #-} 
 
 -- | Higher-level functions to interact with CouchDB views.
+--   To manipulate views in design documents see 
+--   "Database.CouchDB.Conduit.Design"
 
 module Database.CouchDB.Conduit.View 
 (
@@ -9,21 +11,13 @@     -- $run
     couchView,
     couchView',
-    rowValue,
-    -- * Manipulating views
-    couchViewPut
+    rowValue
 )
  where
 
-import              Prelude hiding (catch)
-import              Control.Exception.Lifted (catch)
-
 import              Control.Monad.Trans.Class (lift)
 import              Control.Applicative ((<|>))
 
-import              Data.Default
-import              Data.Generics (Data, Typeable)
-import qualified    Data.Map as Map
 import qualified    Data.ByteString as B
 import qualified    Data.HashMap.Lazy as M
 import qualified    Data.Aeson as A
@@ -38,7 +32,6 @@ 
 import              Database.CouchDB.Conduit
 import              Database.CouchDB.Conduit.LowLevel (couch, protect')
-import qualified    Database.CouchDB.Conduit.Generic as CCG
 
 -----------------------------------------------------------------------------
 -- Running
@@ -100,7 +93,7 @@         (H.RequestBodyBS B.empty) protect'
     bsrc $= conduitCouchView $$ sink
   where
-    fullPath = B.concat ["_design/", designDocName, "/_view/", viewName]
+    fullPath = mkPath ["_design", designDocName, "_view", viewName]
 
 -- | Conduit for extract \"value\" field from CouchDB view row.
 rowValue :: ResourceIO m => Conduit A.Object m A.Value
@@ -110,44 +103,6 @@                         "View row does not contain value: " ++ show v)
 
 -----------------------------------------------------------------------------
--- Manipulators
------------------------------------------------------------------------------
-
--- | View. Just for internal.
-data CouchDesignDoc = CouchDesignDoc {
-      language :: B.ByteString
-    , views :: Map.Map B.ByteString (Map.Map B.ByteString B.ByteString)
-    } deriving (Show, Eq, Data, Typeable)
-    
-instance Default CouchDesignDoc where
-    def = CouchDesignDoc { language = "javascript" ,
-                           views = Map.empty }
-    
--- | Helper for put new views in design documents. 
---
---   /Cauntion/ Current implementation kill all other info except views. Use 
---   wise.
-couchViewPut :: MonadCouch m =>
-       Path                 -- ^ Design document
-    -> Path                 -- ^ View name
-    -> B.ByteString         -- ^ Language. \"javascript\" for example.
-    -> B.ByteString         -- ^ 
-    -> Maybe B.ByteString
-    -> ResourceT m Revision
-couchViewPut designDocName viewName lang mapFn reduceFn = do
-    (rev, dd@(CouchDesignDoc l vs)) <- catch
-        (CCG.couchGet fullPath [])
-        (\(_ :: CouchError) -> return (B.empty, def))
-    if lang /= l then lift $ resourceThrow $ CouchError Nothing "Wrong language"
-        else CCG.couchPut fullPath rev [] $
-                dd { views = Map.insert viewName constructView vs}
-  where
-    fullPath = B.concat ["_design/", designDocName]
-    constructView = let s = Map.singleton "map" mapFn in
-        maybe s (\f -> Map.insert "reduce" f s) reduceFn                          
-            
-
------------------------------------------------------------------------------
 -- Internal Parser conduit
 -----------------------------------------------------------------------------
 
@@ -190,4 +145,5 @@     option () $ skipWhile (\x -> x >= 48 && x <= 57)
     _ <- option "" $ string ","
     _ <- string "\"rows\":["
-    (string "]}" >> return False) <|> return True+    (string "]}" >> return False) <|> return True
+    
test/Database/CouchDB/Conduit/Test/View.hs view
@@ -6,24 +6,34 @@ 
 import Test.Framework (testGroup, mutuallyExclusive, Test)
 import Test.Framework.Providers.HUnit (testCase)
-import Test.HUnit (Assertion)
+import Test.HUnit (Assertion, (@=?))
+import Database.CouchDB.Conduit.Test.Util (setupDB, tearDB)
 
 --import Control.Monad.Trans.Class (lift)
+import Control.Exception.Lifted (bracket_)
 import Control.Monad.IO.Class (liftIO)
+import Control.Applicative ((<$>), (<*>), empty)
 
-import              Data.Generics (Data, Typeable)
+import qualified Data.ByteString as B
+import Data.ByteString.UTF8 (fromString)
+import Data.Aeson ((.:), (.=))
+import qualified Data.Aeson as A
+--import qualified Data.Aeson.Generic as AG
+import Data.Generics (Data, Typeable)
 import Data.Conduit
 import qualified Data.Conduit.List as CL
 
 import Database.CouchDB.Conduit
+import qualified Database.CouchDB.Conduit.Generic as CCG
+import Database.CouchDB.Conduit.DB
 import Database.CouchDB.Conduit.View
-import Control.Monad.Trans.Reader (runReaderT)+import Database.CouchDB.Conduit.Design
 
 tests :: Test
 tests = mutuallyExclusive $ testGroup "View" [
-    testCase "Basic" case_basicViewM
---    testCase "Basic" case_basicView,
---    testCase "Basic with reduce" case_basicViewReduce
+    testCase "Create" case_createView,
+    testCase "Big values parsing" case_bigValues,
+    testCase "View with reduce" case_withReduce
     ]
 
 data T = T {
@@ -32,39 +42,61 @@     strV :: String
     } deriving (Show, Eq, Data, Typeable)
 
-case_manip :: Assertion
-case_manip = runCouch conn $ do
-    r' <- couchViewPut "test" "group3" "javascript" 
-            "function(doc) {emit(null, doc);}" Nothing
-    r'' <- couchViewPut "test1" "group3" "javascript" 
-            "function(doc) {emit(null, doc);}" Nothing
-    liftIO $ print (r', r'')
+instance A.FromJSON T where
+   parseJSON (A.Object v) = T <$> v .: "kind" <*> v .: "intV" <*> v .: "strV"
+   parseJSON _          = empty
+   
+instance A.ToJSON T where
+   toJSON (T k i s) = A.object ["kind" .= k, "intV" .= i, "strV" .= s]
 
-case_basicViewM :: Assertion
-case_basicViewM = withCouchConnection conn . runReaderT . runResourceT $ do
-    s <- couchView "test" "group1" [("reduce", Just "false")] 
-    _ <- s $= (CL.mapM (liftIO . print)) $$ CL.consume
-    s' <- couchView "test" "group1" [("reduce", Just "false")]
-    _ <- s' $= rowValue $$ CL.mapM_ (liftIO . print)
-    res' <- couchView' "test" "group1" [("reduce", Just "false")] $ 
-        rowValue =$ CL.consume
-    liftIO $ print res'
+case_createView :: Assertion
+case_createView = bracket_
+    (setupDB db)
+    (tearDB db) $ runCouch (conn db) $ do
+        rev <- couchViewPut "mydesign" "myview"
+            "function(doc){emit(null, doc);}" Nothing
+        rev' <- CCG.couchRev "_design/mydesign"
+        liftIO $ rev @=? rev' 
+  where 
+    db = "cdbc_test_view_create"
 
-case_basicView :: Assertion
-case_basicView = runCouch conn $  do
-    _ <- couchView' "test" "group1" [("reduce", Just "false")] $ 
-        (CL.mapM (liftIO . print)) =$ CL.consume
-    _ <- couchView' "test" "group1" [("reduce", Just "false")] $ 
-        rowValue =$ CL.mapM_ (liftIO . print)
-    res' <- couchView' "test" "group1" [("reduce", Just "false")] $ 
-        rowValue =$ CL.consume
-    liftIO $ print res'
+case_bigValues :: Assertion
+case_bigValues = bracket_
+    (runCouch (conn db) $ do
+        couchPutDB ""
+        _ <- couchViewPut "mydesign" "myview"
+                "function(doc){emit(doc.intV, doc);}" Nothing
+        mapM_ (\n -> CCG.couchPut' (docName n) [] $ doc n) [1..20]
+    )
+    (tearDB db) $ runCouch (conn db) $ do
+        res <- couchView' "mydesign" "myview" [] $ 
+            (rowValue =$= CCG.toType) =$ CL.consume 
+        mapM_ (\(a, b) -> liftIO $ a @=? doc b) $ zip res [1..20]
+  where 
+    db = "cdbc_test_view_big_values"
+    doc n = T "doc" n $ concat $ replicate 10000 (show n)
 
-case_basicViewReduce :: Assertion
-case_basicViewReduce = runCouch conn $ do
-    res <- couchView' "test" "group1" [] $ 
-        CL.mapM (liftIO . print) =$ CL.consume
-    liftIO $ print res
+data ReducedView = ReducedView Int deriving (Show, Eq, Data, Typeable)
+
+case_withReduce :: Assertion    
+case_withReduce = bracket_
+    (runCouch (conn db) $ do
+        couchPutDB ""
+        _ <- couchViewPut "mydesign" "myview"
+                "function(doc){emit(doc.intV, doc.intV);}" 
+                $ Just "function(keys, values){return sum(values);}"
+        mapM_ (\n -> CCG.couchPut' (docName n) [] $ doc n) [1..20])
+    (tearDB db) $ runCouch (conn db) $ do
+        res <- couchView' "mydesign" "myview" [] $
+            (rowValue =$= CCG.toType) =$ CL.consume
+        liftIO $ res @=? [ReducedView 210]
+  where
+    db = "cdbc_test_view_reduce"
+    doc n = T "doc" n $ show n
     
-conn :: CouchConnection
-conn = def {couchDB = "cdbc_test"} 
+docName :: Int -> B.ByteString
+docName n = fromString $ "doc" ++ show n    
+
+-- | connection
+conn :: Path -> CouchConnection
+conn db = def {couchDB = db}