-- | -- Mostly pure interface to @libgenders@. -- -- Most functions in this module are partial: they may throw 'GendersError'. {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} module Database.Genders ( DB , Attr(..) , attributeName , attributeValue , Query(..) , GendersError(..) , readDB , numNodes , numAttributes , maxAttributes , getSelfNode , nodes , nodesByAttribute , attributes , attributesByNode , query , lookup ) where import Database.Genders.Internal import Data.ByteString (ByteString) import qualified Data.ByteString as BS import qualified Data.ByteString.Char8 as BS8 import Data.Vector (Vector) import qualified Data.Vector as Vector import Control.Applicative import Control.Exception (Exception, throwIO) import Control.Monad (join, when) import Data.Function (on) import Data.List (intercalate) import Data.String (IsString(..)) import Data.Typeable (Typeable) import Foreign (alloca, allocaArray) import Foreign.C (CInt, peekCString, withCString) import Foreign.Ptr (nullPtr) import Foreign.ForeignPtr (ForeignPtr, newForeignPtr, withForeignPtr) import Foreign.Storable (peek, peekElemOff) import GHC.Generics (Generic) import System.IO.Unsafe (unsafePerformIO) import Prelude hiding (lookup) -- Invariant: A DB always has its data pre-loaded into memory using -- genders_load_data(). Therefore querying the DB does not incur side effects. newtype DB = DB (ForeignPtr Handle) -- | An attribute is either standalone or paired with a value. String literals -- are overloaded when @-XOverloadedStrings@ is turned on, so that @"foo" :: -- Attr@ is a legal expression (assumes ASCII encoding). data Attr = Attr ByteString | ByteString :=: ByteString deriving (Eq, Generic, Typeable) instance Show Attr where -- Can't assume any particular encoding, so assume each byte is a character. show (Attr a) = BS8.unpack a show (a :=: v) = intercalate "=" [BS8.unpack a, BS8.unpack v] -- Enable implicit wrapping of strings with Attr. instance IsString Attr where fromString x = Attr (BS8.pack x) attributeName :: Attr -> ByteString attributeName (Attr a) = a attributeName (a :=: _) = a attributeValue :: Attr -> Maybe ByteString attributeValue (Attr _) = Nothing attributeValue (_ :=: v) = Just v -- | Embedded language of genders queries. See @genders_query(3)@ for the -- meaning of each operator. Contrary to the syntax of queries found there, -- operator precedence is the usual one for additive and multiplicative -- operators in our language. data Query = Atom Attr | Query :||: Query | Query :&&: Query | Query :--: Query | Neg Query deriving (Eq, Generic, Typeable) infixl 7 :&&: infixl 6 :||: infixl 6 :--: instance Show Query where show (Atom attr) = show attr show (q1 :||: q2) = "(" ++ intercalate "||" [show q1, show q2] ++ ")" show (q1 :&&: q2) = "(" ++ intercalate "&&" [show q1, show q2] ++ ")" show (q1 :--: q2) = "(" ++ intercalate "--" [show q1, show q2] ++ ")" show (Neg q) = "~" ++ show q instance IsString Query where fromString x = Atom $ Attr $ BS8.pack x data GendersError = GendersError { gendersErrorLocation :: String , gendersErrorMessage :: String } deriving (Show, Generic, Typeable) instance Exception GendersError withHandle :: ForeignPtr Handle -> (Handle -> IO a) -> IO a withHandle fp f = withForeignPtr fp $ \ptr -> f (Handle ptr) throwGendersErrno :: String -> Handle -> IO CInt -> IO Int throwGendersErrno loc h f = do rc <- f when (rc == -1) $ do errcode <- genders_errnum h errstr <- genders_strerror errcode throwIO . GendersError loc =<< peekCString errstr return $ fromIntegral rc -- | Load data from file. readDB :: FilePath -> IO DB readDB filepath = do h@(Handle ptr) <- genders_handle_create -- Register a finalizer to destroy the handle and get genders to reclaim -- memory automatically. fptr <- newForeignPtr genders_handle_destroy'funptr ptr when (ptr == nullPtr) $ throwIO $ GendersError "new" $ "Cannot create new libgenders handle." _ <- withCString filepath $ \cstr -> throwGendersErrno "new" h (genders_load_data h cstr) return $ DB fptr -- | The total number of nodes in the database. numNodes :: DB -> Int numNodes (DB fptr) = unsafePerformIO $ withHandle fptr $ \h -> fromIntegral <$> genders_getnumnodes h -- | The total number of attributes in the database. numAttributes :: DB -> Int numAttributes (DB fptr) = unsafePerformIO $ withHandle fptr $ \h -> fromIntegral <$> genders_getnumattrs h -- | The maximum number of attributes associated with any node. maxAttributes :: DB -> Int maxAttributes (DB fptr) = unsafePerformIO $ withHandle fptr $ \h -> fromIntegral <$> genders_getmaxattrs h -- | The non fully qualified node name of the local host. getSelfNode :: DB -> IO ByteString getSelfNode (DB fptr) = withHandle fptr $ \h -> do maxlen <- throwGendersErrno "getSelfNode" h $ genders_getmaxnodelen h -- The local host might not be in the database, in which case we ought to -- have a minimum buffer size... let len = max maxlen 256 allocaArray len $ \cstr -> do _ <- throwGendersErrno "getSelfNode" h $ genders_getnodename h cstr (fromIntegral len) BS.packCString cstr -- | All nodes in the database. nodes :: DB -> Vector ByteString nodes = nodes' "nodes" Nothing -- | All nodes with the given attribute. nodesByAttribute :: Attr -> DB -> Vector ByteString nodesByAttribute a = nodes' "nodesByAttribute" (Just a) nodes' :: String -> Maybe Attr -> DB -> Vector ByteString nodes' loc mbattr (DB fptr) = unsafePerformIO $ withHandle fptr $ \h -> do alloca $ \ptr -> do n <- throwGendersErrno loc h $ genders_nodelist_create h ptr nodelist <- peek ptr num <- throwGendersErrno loc h $ case mbattr of Nothing -> genders_getnodes h nodelist (fromIntegral n) nullPtr nullPtr Just (Attr a) -> BS.useAsCString a $ \attr -> do genders_getnodes h nodelist (fromIntegral n) attr nullPtr Just (a :=: v) -> BS.useAsCString a $ \attr -> do BS.useAsCString v $ \val -> do genders_getnodes h nodelist (fromIntegral n) attr val result <- Vector.generateM num $ \i -> peekElemOff nodelist i >>= BS.packCString _ <- throwGendersErrno loc h $ genders_nodelist_destroy h nodelist return result attributes :: DB -> Vector Attr attributes = attributes' "attributes" Nothing attributesByNode :: ByteString -> DB -> Vector Attr attributesByNode node = attributes' "attributesByNode" (Just node) attributes' :: String -> Maybe ByteString -> DB -> Vector Attr attributes' loc mbnode (DB fptr) = unsafePerformIO $ withHandle fptr $ \h -> alloca $ \attrlistptr -> do alloca $ \vallistptr -> do nattr <- throwGendersErrno loc h $ genders_attrlist_create h attrlistptr attrlist <- peek attrlistptr nval <- throwGendersErrno loc h $ genders_vallist_create h vallistptr vallist <- peek vallistptr num <- throwGendersErrno loc h $ case mbnode of Nothing -> genders_getattr_all h attrlist (fromIntegral nattr) Just node -> BS.useAsCString node $ \nodestr -> do genders_getattr h attrlist vallist (fromIntegral nval) nodestr result <- Vector.generateM num $ \i -> do a <- peekElemOff attrlist i >>= BS.packCString peekElemOff vallist i >>= \val -> BS.packCString val >>= \v -> return $ if BS.null v then Attr a else a :=: v _ <- throwGendersErrno loc h $ genders_attrlist_destroy h attrlist _ <- throwGendersErrno loc h $ genders_attrlist_destroy h vallist return result -- | Query the database. Example: -- -- @ -- -- Determine the set of nodes that are not management or login nodes. -- query (Neg ("mgmt" :||: "login")) -- @ query :: Query -> DB -> Vector ByteString query q db@(DB fptr) = unsafePerformIO $ withHandle fptr $ \h -> do alloca $ \ptr -> do let n = fromIntegral $ numNodes db _ <- throwGendersErrno "query" h $ genders_nodelist_create h ptr nodelist <- peek ptr withCString (show q) $ \cqstr -> do num <- throwGendersErrno "query" h $ genders_query h nodelist n cqstr result <- Vector.generateM num $ \i -> peekElemOff nodelist i >>= BS.packCString _ <- throwGendersErrno "query" h $ genders_nodelist_destroy h nodelist return result -- | @lookup node attrname attrs@ returns the value of the first occurrence of -- attribute name @attrname@ in @attrs@. lookup :: ByteString -- ^ Attribute name -> Vector Attr -- ^ Sequence of attributes -> Maybe ByteString lookup a = join . fmap attributeValue . Vector.find (on (==) attributeName (Attr a))