packages feed

hasbolt-extras (empty) → 0.0.0.1

raw patch · 18 files changed

+1007/−0 lines, 18 filesdep +basedep +containersdep +hasboltsetup-changed

Dependencies added: base, containers, hasbolt, lens, neat-interpolation, template-haskell, text, th-lift-instances

Files

+ CHANGELOG.md view
@@ -0,0 +1,13 @@+# Changelog+All notable changes to this project will be documented in this file.++The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)+and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).++## [Unreleased]++## [0.1.0.0] - 2018-02-22+### Added+- Template Haskell code to generate `Node`s and `URelationship`s.+- Simple queries to upload `Node` and `URelationship`.+- Simple query to download `Node`s.
+ LICENSE view
@@ -0,0 +1,29 @@+BSD 3-Clause License++Copyright (c) 2018, BIOCAD+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice, this+  list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright notice,+  this list of conditions and the following disclaimer in the documentation+  and/or other materials provided with the distribution.++* Neither the name of the copyright holder nor the names of its+  contributors may be used to endorse or promote products derived from+  this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,1 @@+# hasbolt-extras
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ hasbolt-extras.cabal view
@@ -0,0 +1,50 @@+name:           hasbolt-extras+version:        0.0.0.1+synopsis:       Extras for hasbolt library+description:    Extras for hasbolt library+homepage:       https://github.com/biocad/hasbolt-extras#readme+bug-reports:    https://github.com/biocad/hasbolt-extras/issues+author:         Bogdan Neterebskii, Vladimir Morozov, Sofya Kochkova, Alexander Sadovnikov+maintainer:     neterebskii@gmail.com+copyright:      (c) 2018, BIOCAD+stability:      experimental+category:       Database+license:        BSD3+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10++extra-source-files: CHANGELOG.md+                    README.md++source-repository head+  type: git+  location: https://github.com/biocad/hasbolt-extras++library+  hs-source-dirs:      src+  exposed-modules:     Database.Bolt.Extras+                     , Database.Bolt.Extras.Graph+                     , Database.Bolt.Extras.Persisted+                     , Database.Bolt.Extras.Query+                     , Database.Bolt.Extras.Template+                     , Database.Bolt.Extras.Utils+  other-modules:       Database.Bolt.Extras.Query.Get+                     , Database.Bolt.Extras.Query.Put+                     , Database.Bolt.Extras.Query.Utils+                       Database.Bolt.Extras.Query.Cypher+                  +                     , Database.Bolt.Extras.Template.Types+                     , Database.Bolt.Extras.Template.Instances+                     , Database.Bolt.Extras.Template.Converters+                  +  build-depends:       base >=4.7 && <5+                     , text+                     , hasbolt+                     , containers+                     , neat-interpolation+                     , template-haskell+                     , th-lift-instances+                     , lens+  ghc-options:     -Wall -O2+  default-language: Haskell2010
+ src/Database/Bolt/Extras.hs view
@@ -0,0 +1,14 @@+module Database.Bolt.Extras+  (+    module Database.Bolt.Extras.Graph+  , module Database.Bolt.Extras.Utils+  , module Database.Bolt.Extras.Template+  , module Database.Bolt.Extras.Query+  , module Database.Bolt.Extras.Persisted+  ) where++import Database.Bolt.Extras.Graph+import Database.Bolt.Extras.Utils+import Database.Bolt.Extras.Template+import Database.Bolt.Extras.Query+import Database.Bolt.Extras.Persisted
+ src/Database/Bolt/Extras/Graph.hs view
@@ -0,0 +1,43 @@+{-# LANGUAGE TemplateHaskell #-}++module Database.Bolt.Extras.Graph+  (+    Graph (..)+  , emptyGraph+  , addNode+  , addRelation+  ) where++import           Control.Lens    (makeLenses, over)+import           Data.Map.Strict (Map, insert, notMember)+import           Text.Printf     (printf)++-- | 'Graph' contains vertices, that are parameterized by some type @n@, and relations,+-- that parameterized by pair of type @n@. This pair represents vertices, that are connected with this relation.+--+data Graph n a b = Graph { _vertices  :: Map n a+                         , _relations :: Map (n, n) b+                         } deriving (Show)++makeLenses ''Graph++-- | Creates empty graph.+--+emptyGraph :: Ord n => Graph n a b+emptyGraph = Graph mempty mempty++-- | Adds node to graph by it's @name@ and @node@ content.+-- If graph already contains vertex with given @name@, error will be thrown.+--+addNode :: (Show n, Ord n) => n -> a -> Graph n a b -> Graph n a b+addNode name node graph = if name `notMember` _vertices graph+                          then over vertices (insert name node) graph+                          else error . printf "vertex with name %s key already exists" . show $ name++-- | Adds relation to graph by @startName@ of vertex, @endName@ of vertex, and @rel@ with relation content.+-- If graph already contains relation with given @(startName, endName)@, error will be thrown.+--+addRelation :: (Show n, Ord n) => n -> n -> b -> Graph n a b -> Graph n a b+addRelation startName endName rel graph = if (startName, endName) `notMember` _relations graph+                                          then over relations (insert (startName, endName) rel) graph+                                          else error $ printf "relation with names (%s, %s) already exists" (show startName) (show endName)
+ src/Database/Bolt/Extras/Persisted.hs view
@@ -0,0 +1,50 @@+{-# LANGUAGE DeriveFunctor   #-}+{-# LANGUAGE DeriveGeneric   #-}+{-# LANGUAGE TemplateHaskell #-}++module Database.Bolt.Extras.Persisted+  (+    BoltId+  , Persisted (..)+  , GetBoltId (..)+  , fromInt+  ) where++import           Database.Bolt              (Node (..), Relationship (..),+                                             URelationship (..))+import           Database.Bolt.Extras.Utils (currentLoc)+import           GHC.Generics               (Generic (..))+++-- | 'BoltId' is alias for Bolt 'Node', 'Relationship' and 'URelationship' identities.+--+type BoltId = Int++-- | 'Persisted' is wrapper for some object that can be identified with 'BoltId'.+--+data Persisted a = Persisted { objectId    :: BoltId+                             , objectValue :: a+                             } deriving (Show, Functor, Eq, Ord, Read, Generic)++-- | This is just check that your 'BoltId' is valid.+--+fromInt :: Int -> BoltId+fromInt i | i >= 0    = i+          | otherwise = error $ $currentLoc ++ "could not create BoltId with identity less then zero."++-- Common class to get 'BoltId' from the object.+--+class GetBoltId a where+  getBoltId :: a -> BoltId++instance GetBoltId Node where+  getBoltId = fromInt . nodeIdentity++instance GetBoltId Relationship where+  getBoltId = fromInt . relIdentity++instance GetBoltId URelationship where+  getBoltId = fromInt . urelIdentity++instance GetBoltId (Persisted a) where+  getBoltId = fromInt . objectId
+ src/Database/Bolt/Extras/Query.hs view
@@ -0,0 +1,19 @@+module Database.Bolt.Extras.Query+  ( GraphGetRequest+  , GraphGetResponse+  , GraphPutRequest+  , GraphPutResponse+  , NodeName+  , PutNode (..)+  , ToCypher (..)+  , getGraph+  , putGraph+  ) where++import           Database.Bolt.Extras.Query.Cypher (ToCypher (..))+import           Database.Bolt.Extras.Query.Get    (GraphGetRequest,+                                                    GraphGetResponse, getGraph)+import           Database.Bolt.Extras.Query.Put    (GraphPutRequest,+                                                    GraphPutResponse,+                                                    PutNode (..), putGraph)+import           Database.Bolt.Extras.Query.Utils  (NodeName)
+ src/Database/Bolt/Extras/Query/Cypher.hs view
@@ -0,0 +1,62 @@+{-# LANGUAGE FlexibleInstances    #-}+{-# LANGUAGE OverloadedStrings    #-}+{-# LANGUAGE QuasiQuotes          #-}+{-# LANGUAGE TemplateHaskell      #-}+{-# LANGUAGE TypeSynonymInstances #-}++module Database.Bolt.Extras.Query.Cypher+  (+    ToCypher (..)+  ) where++-------------------------------------------------------------------------------------------------+-- Queries for neo4j are formatted with `Cypher` language.+-- Read documentation for `Cypher` here: https://neo4j.com/docs/developer-manual/current/cypher/.+-- This file contains some converation rules from 'Database.Bolt' types to `Cypher`.+-------------------------------------------------------------------------------------------------++import           Data.Text                         as T (Text, concat, cons,+                                                         intercalate, pack,+                                                         toUpper)+import           Database.Bolt                     (Value (..))+import           Database.Bolt.Extras.Template     (Label, Property)+import           Database.Bolt.Extras.Utils        (currentLoc)+import           NeatInterpolation                 (text)++-- | The class for convertation into Cypher.+--+class ToCypher a where+  toCypher :: a -> Text++-- | Convertation for 'Database.Bolt.Value' into Cypher.+--+instance ToCypher Value where+  toCypher (N ())     = ""+  toCypher (B bool)   = toUpper . pack . show $ bool+  toCypher (I int)    = pack . show $ int+  toCypher (F double) = pack . show $ double+  toCypher (T t)      = [text|"$t"|]+  toCypher (L values) = let cvalues = T.intercalate "," $ map toCypher values+                        in [text|[$cvalues]|]+  toCypher _          = error $ $currentLoc ++ "unacceptable Value type"++-- | Label with @name@ are formatted into @:name@+--+instance ToCypher Label where+  toCypher = cons ':'++-- | Several labels are formatted with concatenation.+--+instance ToCypher [Label] where+  toCypher = T.concat . map toCypher++-- | Converts property with @name@ and @value@ to @name:value@.+--+instance ToCypher Property where+  toCypher (propTitle, value) = T.concat [propTitle, pack ":", toCypher value]++-- | Several properties are formatted with concatenation.+--+instance ToCypher [Property] where+  toCypher = T.intercalate "," . map toCypher+
+ src/Database/Bolt/Extras/Query/Get.hs view
@@ -0,0 +1,131 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}++module Database.Bolt.Extras.Query.Get+    ( GraphGetRequest+    , GraphGetResponse+    , getGraph+    ) where++import           Control.Monad.IO.Class              (MonadIO)+import           Data.Map.Strict                     (Map, keys, mapWithKey,+                                                      toList, (!))+import qualified Data.Text                           as T (Text, concat, empty,+                                                           intercalate, pack)+import           Database.Bolt                       (BoltActionT, Node (..),+                                                      Record, RecordValue (..),+                                                      Relationship (..),+                                                      URelationship (..), exact,+                                                      query)+import           Database.Bolt.Extras.Graph          (Graph (..))+import           Database.Bolt.Extras.Persisted      (BoltId)+import           Database.Bolt.Extras.Query.Cypher   (ToCypher (..))+import           Database.Bolt.Extras.Query.Utils    (NodeName)+import           Database.Bolt.Extras.Template.Types (Label, Property)+import           NeatInterpolation                   (text)+import           Text.Printf                         (printf)++-- | Helper to find 'Node's.+-- _varQNName is the mark for this Node, which will be used in Cypher queries.+-- For example "MATCH(a)", here _varQNName = "a"+data NodeGetter = NodeGetter { boltIdN :: Maybe BoltId+                             , labelsN :: Maybe [Label]+                             , propsN  :: Maybe [Property]+                             } deriving (Show)++-- | RelGetter is used for searching using indexes of 'Node's in the given graph.+data RelGetter = RelGetter { labelR :: Maybe Label+                           , propsR :: Maybe [Property]+                           } deriving (Show)++-- | The combinations of Getters to load graph from the database.+type GraphGetRequest = Graph NodeName NodeGetter RelGetter++type GraphGetResponse = Graph NodeName Node URelationship++-- | For the given GraphGetRequest find the graph, which matches it.+-- This function creates single cypher query and performs it.+getGraph :: (MonadIO m) => [T.Text] -> GraphGetRequest -> BoltActionT m [GraphGetResponse]+getGraph customConds requestGraph = do+  response <- query (formQuery customConds nodeVars edgesVars vertices rels)+  mapM (\i -> do+      nodes <- sequence $ mapOnlyKey (fmap (!! i) . flip exactValues response) vertices+      edges <- sequence $ mapOnlyKey (fmap (makeU . (!! i)) . flip exactValues response . namesToText) rels+      return (Graph nodes edges)) [0.. length response - 1]+  where+    vertices :: Map NodeName NodeGetter+    vertices = _vertices requestGraph++    rels :: Map (NodeName, NodeName) RelGetter+    rels = _relations requestGraph++    nodeVars :: [T.Text]+    nodeVars = keys vertices++    edgesVars :: [T.Text]+    edgesVars = map (\k -> T.concat [fst k, "0", snd k]) (keys rels)++    exactValues :: (MonadIO m, RecordValue a) => T.Text -> [Record] -> BoltActionT m [a]+    exactValues var = mapM (exact . (! var))++    makeU :: Relationship -> URelationship+    makeU Relationship{..} = URelationship relIdentity relType relProps++    namesToText :: (NodeName, NodeName) -> T.Text+    namesToText (nameA, nameB) = T.concat [nameA, "0", nameB]++    mapOnlyKey :: (k -> b) -> Map k a -> Map k b+    mapOnlyKey f = mapWithKey (\k _ -> f k)++formQuery :: [T.Text] -> [T.Text] -> [T.Text] -> Map NodeName NodeGetter -> Map (NodeName, NodeName) RelGetter -> T.Text+formQuery customConds returnNodes returnEdges vertices rels =+  [text|MATCH $completeRequest+        $conditionsQ+        RETURN $completeResponse|]+  where+    nodes = nodeAsText <$> toList vertices++    conditionsId     = intercalateAnd . filter (/= "\n") $ fmap condIdAsText (toList vertices)+    customConditions = intercalateAnd customConds+    conditions       = intercalateAnd . filter (/= T.empty) $ [conditionsId, customConditions]+    conditionsQ      = if conditions == T.empty then "" else T.concat ["WHERE ", conditions]++    edges = fmap (relationshipAsText vertices) (toList rels)++    completeRequest  = T.intercalate "," $ nodes ++ edges+    completeResponse = T.intercalate "," $ returnNodes ++ returnEdges++    intercalateAnd :: [T.Text] -> T.Text+    intercalateAnd = T.intercalate " AND "++condIdAsText :: (NodeName, NodeGetter) -> T.Text+condIdAsText (name, sel) = [text|$boltIdNR|]+  where+    boltIdNR = maybeNull (T.pack . printf "ID(%s)=%d" name) (boltIdN sel)++nodeAsText :: (NodeName, NodeGetter) -> T.Text+nodeAsText (name, sel) = [text|($name $labels $propsQ)|]+  where+    labels = maybeNull toCypher (labelsN sel)+    propsQ = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsN sel)++relationshipAsText :: Map NodeName NodeGetter -> ((NodeName, NodeName), RelGetter) -> T.Text+relationshipAsText vertices ((begNodeName, endNodeName), uRel) =+  [text|($begNodeName $begNodeLabels $begNodeProps)-[$name $typeQ $propsQ]-($endNodeName $endNodeLabels $endNodeProps)|]+  where+    name   = T.concat [begNodeName, "0", endNodeName]+    typeQ  = maybeNull toCypher (labelR uRel)++    begNode       = vertices ! begNodeName+    begNodeLabels = maybeNull toCypher $ labelsN begNode+    begNodeProps  = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsN begNode)++    endNode       = vertices ! endNodeName+    endNodeLabels = maybeNull toCypher $ labelsN endNode+    endNodeProps  = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsN endNode)++    propsQ = maybeNull (\props -> T.concat ["{", toCypher props, "}"]) (propsR uRel)++maybeNull :: (a -> T.Text) -> Maybe a -> T.Text+maybeNull = maybe ""
+ src/Database/Bolt/Extras/Query/Put.hs view
@@ -0,0 +1,96 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE QuasiQuotes       #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE TemplateHaskell   #-}++module Database.Bolt.Extras.Query.Put+    ( GraphPutRequest+    , GraphPutResponse+    , PutNode (..)+    , putGraph+    ) where++import           Control.Monad                     (forM)+import           Control.Monad.IO.Class            (MonadIO)+import           Data.Map.Strict                   (mapWithKey, toList, (!))+import qualified Data.Map.Strict                   as M (map)+import qualified Data.Text                         as T (Text, pack)+import           Database.Bolt                     (BoltActionT, Node (..),+                                                    RecordValue (..),+                                                    URelationship (..), at,+                                                    exact, query)+import           Database.Bolt.Extras.Graph        (Graph (..))+import           Database.Bolt.Extras.Persisted    (BoltId, fromInt)+import           Database.Bolt.Extras.Query.Cypher (ToCypher (..))+import           Database.Bolt.Extras.Query.Utils  (NodeName)+import           NeatInterpolation                 (text)++-- | For given @Node _ labels nodeProps@ makes query @MERGE (n:labels {props}) RETURN ID(n) as n@+-- and then return 'Node' with actual ID.+--+-- Potentially, if you MERGE some 'Node' and it labels and props are occured in+-- several 'Node's, then the result can be not one but several 'Node's.+--+data PutNode = BoltId BoltId | Merge Node | Create Node+  deriving (Show)++type GraphPutRequest = Graph NodeName PutNode URelationship++type GraphPutResponse = Graph NodeName BoltId BoltId++putNode :: (MonadIO m) => PutNode -> BoltActionT m [BoltId]+putNode ut = case ut of+    (BoltId bId)  -> pure [bId]+    (Merge node)  -> helper (T.pack "MERGE") node+    (Create node) -> helper (T.pack "CREATE") node+  where+    helper :: (MonadIO m) => T.Text -> Node -> BoltActionT m [BoltId]+    helper q node = do+      let varQ  = "n"++      let labelsQ = toCypher $ labels node+      let propsQ  = toCypher . toList $ nodeProps node++      let getQuery = [text|$q ($varQ $labelsQ {$propsQ})+                           RETURN ID($varQ) as $varQ|]++      records <- query getQuery+      forM records $ \record -> do+        nodeIdentity' <- record `at` varQ >>= exact+        pure $ fromInt nodeIdentity'++-- | Every relationship in Bolt protocol starts from one 'Node' and ends in anoter.+-- For given starting and ending 'Node's, and for @URelationship  _ urelType urelProps@+-- this method makes MERGE query and then return 'Relationship' with actual ID.+putRelationship :: (MonadIO m) => BoltId -> URelationship -> BoltId -> BoltActionT m BoltId+putRelationship start URelationship{..} end = do+  [record]      <- query mergeQ+  urelIdentity' <- record `at` varQ >>= exact+  pure $ fromInt urelIdentity'+  where+    varQ = "r"+    labelQ = toCypher urelType+    propsQ = toCypher . toList $ urelProps+    startT = T.pack . show $ start+    endT = T.pack . show $ end++    mergeQ :: T.Text+    mergeQ = [text|MATCH (a), (b)+              WHERE ID(a) = $startT AND ID(b) = $endT+              MERGE (a)-[$varQ $labelQ {$propsQ}]->(b)+              RETURN ID($varQ) as $varQ|]++-- | Create Graph using given GraphU and the list describing 'Node's indices (from the given _vertices),+-- which should be connected by the corresponding 'Relationship'.+-- If there were multiple choices while merging given _vertices, the first match is used for connection.+putGraph :: (MonadIO m) => GraphPutRequest -> BoltActionT m GraphPutResponse+putGraph requestGraph = do+  let vertices = _vertices requestGraph+  let rels = _relations requestGraph+  nodes <- sequenceA $ M.map (fmap head . putNode) vertices+  edges <- sequenceA $+          mapWithKey (\key v -> do+              let stNode  = nodes ! fst key+              let endNode = nodes ! snd key+              putRelationship stNode v endNode) rels+  return $ Graph nodes edges
+ src/Database/Bolt/Extras/Query/Utils.hs view
@@ -0,0 +1,7 @@+module Database.Bolt.Extras.Query.Utils+  ( NodeName+  ) where++import           Data.Text (Text)++type NodeName = Text
+ src/Database/Bolt/Extras/Template.hs view
@@ -0,0 +1,31 @@+module Database.Bolt.Extras.Template+  ( FromValue (..)+  , Label+  , Labels (..)+  , Node (..)+  , NodeLike (..)+  , Properties (..)+  , Property+  , Relationship (..)+  , ToValue (..)+  , URelationLike (..)+  , URelationship (..)+  , Value (..)+  , makeNodeLike+  , makeURelationLike+  ) where++import           Database.Bolt.Extras.Template.Converters (makeNodeLike,+                                                           makeURelationLike)+import           Database.Bolt.Extras.Template.Instances  ()+import           Database.Bolt.Extras.Template.Types      (FromValue (..),+                                                           Label, Labels (..),+                                                           Node (..),+                                                           NodeLike (..),+                                                           Properties (..),+                                                           Property,+                                                           Relationship (..),+                                                           ToValue (..),+                                                           URelationLike (..),+                                                           URelationship (..),+                                                           Value (..))
+ src/Database/Bolt/Extras/Template/Converters.hs view
@@ -0,0 +1,269 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++module Database.Bolt.Extras.Template.Converters+ (+    makeNodeLike+  , makeURelationLike+  ) where++import           Control.Lens                        (view, _1)+import           Data.Map.Strict                     (fromList, member, (!))+import           Data.Text                           (Text, pack, unpack)+import           Database.Bolt.Extras.Template.Types (FromValue (..),+                                                      Labels (..), Node (..),+                                                      NodeLike (..),+                                                      Properties (..),+                                                      ToValue (..),+                                                      URelationLike (..),+                                                      URelationship (..))+import           Database.Bolt.Extras.Utils          (currentLoc, dummyId)+import           Instances.TH.Lift                   ()+import           Language.Haskell.TH++-- | Describes a @bijective@ class, i.e. class that has two functions: @phi :: a -> SomeType@ and @phiInv :: SomeType -> a@.+-- Requires class name, @SomeType@ name and names of the class functions (@phi@ and @phiInv@).+--+data BiClassInfo = BiClassInfo { className    :: Name+                               , dataName     :: Name+                               , classToFun   :: Name+                               , classFromFun :: Name+                               }++-- | Example of @bijective@ class is 'NodeLike'.+-- Describes conversions into and from 'Node'.+-- That is, this class provides a bridge between Neo4j world and Haskell world.+--+nodeLikeClass :: BiClassInfo+nodeLikeClass = BiClassInfo { className     = ''NodeLike+                            , dataName      = 'Node+                            , classToFun    = 'toNode+                            , classFromFun  = 'fromNode+                            }++-- | Another example of @bijective@ class is 'URelationLike'.+-- Describes conversions into and from 'URelationship'.+--+uRelationLikeClass :: BiClassInfo+uRelationLikeClass = BiClassInfo { className    = ''URelationLike+                                 , dataName     = 'URelationship+                                 , classToFun   = 'toURelation+                                 , classFromFun = 'fromURelation+                                 }++-- | Make an instance of 'NodeLike' class.+-- Only data types with one constructor are currently supported.+-- Each field is transformed into 'Text' key and its value is transformed into a 'Value'.+-- For example, we have a structure+--+-- > data Foo = Bar { baz  :: Double+-- >                , quux :: Text+-- >                , quuz :: Int+-- >                }+--+-- You can make it instance of NodeClass by writing+-- > makeNodeLike ''Foo+--+-- Then you may create example and convert it into from from Node:+--+-- > ghci>:set -XOverloadedStrings+-- > ghci> let foo = Bar 42.0 "Loren ipsum" 7+-- > ghci> toNode foo+-- > Node {nodeIdentity = -1, labels = ["Foo"], nodeProps = fromList [("baz",F 42.0),("quux",T "Loren ipsum"),("quuz",I 7)]}+-- > ghci> fromNode . toNode $ foo :: Foo+-- > Bar {baz = 42.0, quux = "Loren ipsum", quuz = 7}+--+makeNodeLike :: Name -> Q [Dec]+makeNodeLike = makeBiClassInstance nodeLikeClass+++-- | Make an instance of 'URelationLike' class.+-- Transformations are the same as in 'NodeLike' instance declaration with the only one difference:+-- 'URelationship' holds only one label (or type), but 'Node' holds list of labels.+--+makeURelationLike :: Name -> Q [Dec]+makeURelationLike = makeBiClassInstance uRelationLikeClass+++-- | Declare an instance of `bijective` class using TemplateHaskell.+-- It works as follows:+-- Say we have a type with field records, e.g.+--+-- > data VariableDomainScoring = VDS { specie   :: Text+-- >                                  , vgen     :: Double+-- >                                  , fr       :: Double+-- >                                  , sim      :: Double+-- >                                  , germline :: Text+-- >                                  }+--+-- As an example, transformation into Node is described below.+--+-- > data Node = Node { nodeIdentity :: Int             -- ^Neo4j node identifier+-- >                  , labels       :: [Text]          -- ^Set of node labels (types)+-- >                  , nodeProps    :: Map Text Value  -- ^Dict of node properties+-- >                  }+-- >  deriving (Show, Eq)+--+-- @nodeIdentity@ will be set to a dummy value (-1). There is no way of obtaining object ID before uploading it into database.+-- @labels@ will be set to type name, i.e. @VariableDomainScoring@. This is due to our convention: object label into Neo4j is the same as its type name in Haskell.+-- @nodeProps@ will be set to a Map: keys are field record names, values are data in the corresponding fields.+--+-- Therefore, applying toNode on a @VariableDomainScoring@ will give the following:+-- > Node { nodeIdentity = -1+-- >      , labels = ["VariableDomainScoring"]+-- >      , nodeProps = fromList [("specie", T "text value"), ("vgen", F %float_value), ("fr", F %float_value), ("sim", F %float_value), ("germline", T "text value")]+-- >     }+--+makeBiClassInstance :: BiClassInfo -> Name -> Q [Dec]+makeBiClassInstance BiClassInfo {..} typeCon = do+  -- reify function gives Info about Name such as constructor name and its fields. See: https://hackage.haskell.org/package/template-haskell-2.12.0.0/docs/Language-Haskell-TH.html#t:Info+  TyConI declaration <- reify typeCon++  -- get type declaration parameters: type name and fields. Supports data and newtype only. These will be used in properties Map formation.+  let (tyName, constr) = getTypeCons declaration++  -- nameBase gives object name without package prefix. `label` is the type name here.+  let label = nameBase tyName++  -- collects the names of all fields in the type.+  let dataFields = concatMap (snd . getConsFields) constr++  -- gets data constructor name+  let (consName, _) = head $ fmap getConsFields constr++  -- Just a fresh variable. It will be used in labmda abstractions in makeToClause and makeFromClause functions.+  fresh <- newName "x"++  -- constructs `bijective` class functions (phi and phiInv – toClause and fromClause correspondingly here).+  toClause   <- makeToClause label dataName fresh dataFields+  fromClause <- makeFromClause label consName fresh dataFields++  -- function declarations themselves.+  let bodyDecl = [FunD classToFun [toClause], FunD classFromFun [fromClause]]++  -- Instance declaration itself.+  pure [InstanceD Nothing [] (AppT (ConT className) (ConT typeCon)) bodyDecl]++++-- | Extract information about type: constructor name and field record names.+--+getConsFields :: Con -> (Name, [Name])+getConsFields (RecC cName decs)           = (cName, fmap (view _1) decs)+getConsFields (ForallC _ _ cons)          = getConsFields cons+getConsFields (RecGadtC (cName:_) decs _) = (cName, fmap (view _1) decs)+getConsFields (NormalC cName _)           = (cName, [])+getConsFields _                           = error $ $currentLoc ++ "unsupported data declaration."+++-- | Parse a type declaration and retrieve its name and its constructors.+--+getTypeCons :: Dec -> (Name, [Con])+getTypeCons (DataD    _ typeName _ _ constructors _) = (typeName, constructors)+getTypeCons (NewtypeD _ typeName _ _ constructor  _) = (typeName, [constructor])+getTypeCons otherDecl = error $ $currentLoc ++ "unsupported declaration: " ++ show otherDecl ++ "\nShould be either 'data' or 'newtype'."++-- | Describes the body of conversion to target type function.+--+makeToClause :: String -> Name -> Name -> [Name] -> Q Clause+makeToClause label dataCons varName dataFields | null dataFields = pure $ Clause [WildP] (NormalB result) []+                                               | otherwise       = pure $ Clause [VarP varName] (NormalB result) []+  where+    -- apply field record to a data.+    getValue :: Name -> Exp+    getValue name = AppE (VarE name) (VarE varName)++    -- List of values which a data holds.+    -- The same in terms of Haskell :: valuesExp = fmap (\field -> toValue (field x))+    -- `x` is a bounded in pattern match variable (e.g. toNode x = ...). If toNode :: a -> Node, then x :: a, i.e. x is data which we want to convert into Node.+    -- `field` is a field record function.+    valuesExp :: [Exp]+    valuesExp = fmap (\fld -> AppE (VarE 'toValue) (getValue fld)) dataFields++    -- Retrieve all field record names from the convertible type.+    fieldNames :: [String]+    fieldNames = fmap nameBase dataFields++    -- List of pairs :: [(key, value)]+    -- `key` is field record name.+    -- `value` is the data that corresponding field holds.+    pairs :: [Exp]+    pairs = zipWith (\fld val -> TupE [strToTextE fld, val]) fieldNames valuesExp++    -- Map representation:+    -- mapE = fromList pairs+    -- in terms of Haskell.+    mapE :: Exp+    mapE = AppE (VarE 'fromList) (ListE pairs)++    -- A bit of crutches.+    -- The difference between Node and URelationship is in the number of labels they hold.+    -- strToTextE returns Text packed in Exp so `id` is applied to it when constructing URelationship.+    -- Node takes list of labels so the label must be packed into list using ListE . (:[])+    fieldFun :: Exp -> Exp+    fieldFun | nameBase dataCons == "URelationship" = id+             | nameBase dataCons == "Node"          = ListE . (:[])+             | otherwise                = error $ $currentLoc ++ "unsupported data type."++    -- In terms of Haskell:+    -- dataCons (fromIntegral dummyId) (fieldFun label) mapE+    -- Constructs data with three fields.+    result :: Exp+    result = AppE (AppE (AppE (ConE dataCons) (LitE . IntegerL . fromIntegral $ dummyId)) (fieldFun $ strToTextE label)) mapE+++-- | Describes the body of conversion from target type function.+--+makeFromClause :: String -> Name -> Name -> [Name] -> Q Clause+makeFromClause label conName varName dataFields = do+  -- fieldNames :: [Text]+  -- field records of the target type.+  let fieldNames = fmap (pack . nameBase) dataFields++  -- dataLabel :: Text+  -- Label a.k.a type name+  let dataLabel = pack label++  -- Field record names packed in Exp+  -- \x -> [|x|] :: a -> Q Exp+  -- Therefore, fieldNamesE :: [Exp]+  fieldNamesE <- mapM (\x -> [|x|]) fieldNames++  -- varExp :: Q Exp+  -- Pattern match variable packed in Exp. It will be used in QuasiQuotation below.+  let varExp = pure (VarE varName)++  -- Guard checks that all necessary fields are present in the container.+  guardSuccess <- NormalG <$> [|checkLabels $(varExp) [dataLabel] && checkProps $(varExp) fieldNames|]++  -- `otherwise` case.+  guardFail <- NormalG <$> [|otherwise|]++  -- Unpack error message.+  failExp <- [|unpackError $(varExp) (unpack dataLabel)|]++  -- Kind of this function realization in terms of Haskell:+  -- fromNode :: Node -> a+  -- fromNode varName | checkLabels varName [dataLabel] && checkProps varName fieldNames = ConName { foo = bar, baz = quux ...}+  --                  | otherwise = unpackError varName (unpack dataLabel)+  let successExp = RecConE conName (zipWith (\f d -> (d, AppE (AppE (VarE 'getProp) (VarE varName)) f)) fieldNamesE dataFields)+  let successCase = (guardSuccess, successExp)+  let failCase = (guardFail, failExp)++  pure $ Clause [VarP varName] (GuardedB [successCase, failCase]) []+++strToTextE :: String -> Exp+strToTextE str = AppE (VarE 'pack) (LitE . StringL $ str)++checkProps :: Properties t => t -> [Text] -> Bool+checkProps container = all (`member` getProps container)++checkLabels :: Labels t => t -> [Text] -> Bool+checkLabels container = all (`elem` getLabels container)++getProp :: (Properties t, FromValue a) => t -> Text -> a+getProp container field = fromValue (getProps container ! field)++unpackError :: Show c => c -> String -> a+unpackError container label = error $ $currentLoc ++ " could not unpack " ++ label ++ " from " ++ show container
+ src/Database/Bolt/Extras/Template/Instances.hs view
@@ -0,0 +1,93 @@+{-# LANGUAGE FlexibleInstances #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}+{-# OPTIONS_GHC -fno-warn-orphans #-}++module Database.Bolt.Extras.Template.Instances () where++import           Data.Map.Strict                     (Map)+import           Data.Text                           (Text)+import           Database.Bolt                       (Value (..))+import qualified Database.Bolt                       as DB (Structure)+import           Database.Bolt.Extras.Template.Types (FromValue (..),+                                                      ToValue (..))+import           Database.Bolt.Extras.Utils          (currentLoc)+import           GHC.Float                           (double2Float,+                                                      float2Double)++instance ToValue () where+  toValue = N++instance ToValue Bool where+  toValue = B++instance ToValue Int where+  toValue = I++instance ToValue Double where+  toValue = F++instance ToValue Float where+  toValue = F . float2Double++instance ToValue Text where+  toValue = T++instance ToValue Value where+  toValue = id++instance ToValue a => ToValue [a] where+  toValue = L . fmap toValue++instance ToValue a => ToValue (Maybe a) where+  toValue (Just a) = toValue a+  toValue _        = toValue ()++instance ToValue (Map Text Value) where+  toValue = M++instance ToValue DB.Structure where+  toValue = S++instance FromValue () where+  fromValue (N ()) = ()+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into ()"++instance FromValue Bool where+  fromValue (B boolV) = boolV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into Bool"++instance FromValue Int where+  fromValue (I intV) = intV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into Int"++instance FromValue Double where+  fromValue (F doubleV) = doubleV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into Double"++instance FromValue Float where+  fromValue (F doubleV) = double2Float doubleV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into Float"++instance FromValue Text where+  fromValue (T textV) = textV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into Text"++instance FromValue Value where+  fromValue = id++instance FromValue a => FromValue [a] where+  fromValue (L listV) = fmap fromValue listV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into [Value]"++instance FromValue a => FromValue (Maybe a) where+  fromValue (N ()) = Nothing+  fromValue a = Just $ fromValue a++instance FromValue (Map Text Value) where+  fromValue (M mapV) = mapV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into (Map Text Value)"++instance FromValue DB.Structure where+  fromValue (S structureV) = structureV+  fromValue v      = error $ $currentLoc ++ "could not unpack " ++ show v ++ " into Structure"
+ src/Database/Bolt/Extras/Template/Types.hs view
@@ -0,0 +1,66 @@+module Database.Bolt.Extras.Template.Types+  (+    FromValue (..)+  , Label+  , Labels (..)+  , Node (..)+  , NodeLike (..)+  , Properties (..)+  , Property+  , Relationship (..)+  , ToValue (..)+  , URelationLike (..)+  , URelationship (..)+  , Value (..)+  ) where++import           Data.Map.Strict (Map)+import           Data.Text       (Text)+import           Database.Bolt   (Node (..), Relationship (..),+                                  URelationship (..), Value (..))++-- | Alias for Neo4j label+type Label = Text++-- | Alias for Neo4j property+type Property = (Text, Value)++-- | 'NodeLike' class represents convertable into and from 'Node'.+class NodeLike a where+  toNode :: a -> Node+  fromNode :: Node -> a++-- | 'URelationLike' class represents convertable into and from 'URelationship'.+class URelationLike a where+  toURelation :: a -> URelationship+  fromURelation :: URelationship -> a++-- | 'ToValue' means that something can be converted into Bolt 'Value'.+class ToValue a where+  toValue :: a -> Value++-- | 'FromValue' means that something can be converted from Bolt 'Value'.+class FromValue a where+  fromValue :: Value -> a++class Labels a where+  getLabels :: a -> [Label]++instance Labels Node where+  getLabels = labels++instance Labels URelationship where+  getLabels = pure . urelType++class Properties a where+  getProps :: a -> Map Text Value++instance Properties Node where+  getProps = nodeProps++instance Properties URelationship where+  getProps = urelProps++++
+ src/Database/Bolt/Extras/Utils.hs view
@@ -0,0 +1,31 @@+module Database.Bolt.Extras.Utils+  (+    dummyId+  , union+  , currentLoc+  ) where++import           Data.List           (nub)+import qualified Data.Map.Strict     as M (union)+import           Database.Bolt       (Node (..))+import           Language.Haskell.TH (Exp (..), Lit (..), Loc (..), Q, location)+import           Text.Printf         (printf)+++-- | 'dummyId' is used to load 'Node' and 'URelationship' into database,+-- because id from database is not known for such moment.+dummyId :: Int+dummyId = -1++-- | 'Node's can be merged. 'union' is useful when you have to store in one node+-- several labels and props from different classes.+union :: Node -> Node -> Node+(Node _ labels1 props1) `union` (Node _ labels2 props2) = Node dummyId+                                                               (nub $ labels1 ++ labels2)+                                                               (props1 `M.union` props2)++-- | 'currentLoc' shows module name and line where this expression is used.+currentLoc :: Q Exp+currentLoc = do+  loc <- location+  pure $ LitE $ StringL $ printf "%s:%d: " (loc_module loc) (fst $ loc_start loc)