packages feed

convert-annotation-0.5.0.1: app/Main.hs

{- convert-annotation
Gregory W. Schwartz

Converts an unknown annotation to Ensembl's annotation, or other annotation.
-}

{-# LANGUAGE BangPatterns      #-}
{-# LANGUAGE DataKinds         #-}
{-# LANGUAGE DeriveGeneric     #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators     #-}

module Main where

-- Standard
import Data.Maybe
import Data.Char
import Data.List
import Control.Monad
import GHC.Generics
import Data.Semigroup

-- Cabal
import qualified Data.Vector as V
import qualified Data.ByteString.Lazy.Char8 as B
import qualified Data.Text as T
import qualified Data.Csv as CSV
import qualified Control.Lens as L
import Pipes
import qualified Pipes.Prelude as P
import qualified Pipes.ByteString as PB
import Pipes.Csv
import Options.Generic

import qualified Foreign.R as R
import Foreign.R (SEXP, SEXPTYPE)
import Language.R.Instance as R
import Language.R.QQ
import Language.R.Literal as R

-- Local
import Types
import EnsemblConvert
import HUGOConvert
import UniProtConvert
import RGeneConvert
import MSigDBRDataConvert

-- | Command line arguments
data Options = Info { delimiter        :: Maybe String
                                      <?> "([,] | CHAR) The delimiter of the CSV file."
                    , database         :: String
                                      <?> "(Ensembl | HUGO TYPE | UniProt | RGene (TYPE, TYPE) | MSigDBRdata (FILE, RDATA, TYPE)) Which database to convert with. TYPE is the type of the original gene symbol. The compatible list for TYPE with HUGO is in http://www.genenames.org/help/rest-web-service-help. HUGO is only supported for Annotation. RGene (Annotation only) takes in a type of (FROM, TO) for the gene symbol origin and destination. MSigDBRdata (Info only) takes an rdata file (tested with http://bioinf.wehi.edu.au/software/MSigDB/), the name of the rdata object containing the named list, and the TYPE of symbol (compatable list at http://bioconductor.org/packages/release/bioc/manuals/biomaRt/man/biomaRt.pdf in getGene) which returns pathways separated by \"/\"."
                    , descriptionField :: Maybe String
                                      <?> "(Other TEXT | Description | Synonyms) The info to retrieve about the identifier. Description provides information about the identifier while synonyms provides alternate identifiers for the same entity. Returns a list of information (delimited by '/') for each match to Ensembl's cross references. For UniProt, enter a valid column (http://www.uniprot.org/help/programmatic_access)."
                    , column           :: T.Text
                                      <?> "(COLUMN) The column containing the identifier. Must be a valid id for info."
                    , newColumn        :: Maybe T.Text
                                      <?> "([Nothing] | COLUMN) The new column to put the results into. If unspecified, replaces the original column."
                    , remove           :: Bool
                                      <?> "Whether to remove empty results (no matches to the database)."
                    , strict           :: Bool
                                      <?> "Whether to load everything in memory, no streaming. Useful for the R conversions only."
                    }
             | Annotation { delimiter :: Maybe String
                                     <?> "([,] | CHAR) The delimiter of the CSV file."
                          , database  :: String
                                     <?> "(Ensembl | HUGO TYPE | UniProt | RGene (TYPE, TYPE) | MSigDBRdata (FILE, RDATA, TYPE)) Which database to convert with. TYPE is the type of the original gene symbol. The compatible list for TYPE with HUGO is in http://www.genenames.org/help/rest-web-service-help. HUGO is only supported for Annotation. RGene (Annotation only) takes in a type of (FROM, TO) for the gene symbol origin and destination. MSigDBRdata (Info only) takes an rdata file (tested with http://bioinf.wehi.edu.au/software/MSigDB/), the name of the rdata object containing the named list, and the TYPE of symbol (compatable list at http://bioconductor.org/packages/release/bioc/manuals/biomaRt/man/biomaRt.pdf in getGene) which returns pathways separated by \"/\"."
                          , column    :: T.Text
                                     <?> "(COLUMN) The column containing the identifier. Must be a valid id for info."
                          , newColumn :: Maybe T.Text
                                     <?> "([Nothing] | COLUMN) The new column to put the results into. If unspecified, replaces the original column."
                          , remove    :: Bool
                                     <?> "Whether to remove empty results (no matches to the database)."
                          , strict    :: Bool
                                     <?> "Whether to load everything in memory, no streaming. Useful for the R conversions only."
                          }
               deriving (Generic)

instance ParseRecord Options

-- | Map the header column to the rest of the file for converting that
-- column.
pipeConvert :: Options
            -> Maybe (RMart s)
            -> Maybe (RData s)
            -> Pipe [T.Text] [T.Text] IO ()
pipeConvert opts rMart rData = do
    h <- await

    let c      = col opts h
        newCol = unHelpful . newColumn $ opts

    yield . maybe h (\x -> h <> [x]) $ newCol

    forever $ do
        x    <- await
        newX <- lift . convertSingle opts rMart rData . (!! c) $ x
        unless ((unHelpful . remove $ opts) && T.null newX)
            . maybe (yield . L.set (L.ix c) newX $ x)
                    (const (yield (x <> [newX])))
            $ newCol
        return ()

    return ()

-- | Get the index of the column.
col :: Options -> [T.Text] -> Int
col opts =
    fromMaybe (error "Column not found.") . elemIndex (unHelpful $ column opts)

-- | Convert the entire file at once, no streaming.
strictConvert :: Options
              -> Maybe (RMart s)
              -> Maybe (RData s)
              -> [[T.Text]]
              -> IO ()
strictConvert opts rMart rData (h:body) = do
    let c      = col opts h
        newCol = unHelpful . newColumn $ opts

    let newH  = maybe h (\x -> h <> [x]) $ newCol
        xs    = fmap (!! c) body

    newXS <- convertMultiple opts rMart rData $ xs

    let addToRow newX row =
            if (unHelpful . remove $ opts) && (T.null newX)
                then Nothing
                else Just
                   . maybe (L.set (L.ix c) newX row) (\x -> row <> [newX])
                   $ newCol
        newBody          = catMaybes . zipWith addToRow newXS $ body

    B.putStrLn . CSV.encode . (:) newH $ newBody

-- | The conversion process for streaming.
convertSingle :: Options
              -> Maybe (RMart s)
              -> Maybe (RData s)
              -> T.Text
              -> IO T.Text
convertSingle opts@(Info { descriptionField = df }) rMart rData =
    fmap (fromMaybe "" . fmap unDesc)
        . whichDesc (read . unHelpful . database $ opts)
        . UnknownAnn
  where
    whichDesc Ensembl  = toEnsemblDesc ( read
                                       . fromMaybe (error "Needs description field.")
                                       . unHelpful
                                       $ df
                                       )
    whichDesc (HUGO _) = error "HUGO description not yet supported."
    whichDesc UniProt  = toUniProtDesc ( read
                                       . fromMaybe (error "Needs description field.")
                                       . unHelpful
                                       $ df
                                       )
    whichDesc (RGene _) = error "RGene description not yet supported."
    whichDesc (MSigDBRData queryType) =
        toMSigDBPathways
            (fromJust rData)
            (fromJust rMart)
            (MSigDBType queryType)
convertSingle opts@(Annotation {}) rMart rData                  =
    fmap (fromMaybe "" . fmap unAnn)
        . whichAnn (read . unHelpful . database $ opts)
        . UnknownAnn
  where
    whichAnn Ensembl           = toEnsemblAnn
    whichAnn (HUGO queryType)  = toHUGOAnn . HUGOType $ queryType
    whichAnn UniProt           = toUniProtAnn
    whichAnn (RGene queryType) =
        toRGeneAnn (fromJust rMart) (RType queryType)
    whichAnn (MSigDBRData _)   =
        error "MSigDBRData annotation not yet supported."

-- | The conversion process for all in memory.
convertMultiple :: Options
                -> Maybe (RMart s)
                -> Maybe (RData s)
                -> [T.Text]
                -> IO [T.Text]
convertMultiple opts@(Info { descriptionField = df }) rMart rData =
    fmap (fmap (fromMaybe "" . fmap unDesc))
        . whichDesc (read . unHelpful . database $ opts)
        . fmap UnknownAnn
  where
    whichDesc Ensembl  =
        mapM ( toEnsemblDesc ( read
                             . fromMaybe (error "Needs description field.")
                             . unHelpful
                             $ df
                             )
             )
    whichDesc (HUGO _) = error "HUGO description not yet supported."
    whichDesc UniProt  =
        mapM (toUniProtDesc ( read
                            . fromMaybe (error "Needs description field.")
                            . unHelpful
                            $ df
                            )
             )
    whichDesc (RGene _) = error "RGene description not yet supported."
    whichDesc (MSigDBRData queryType) =
        toMSigDBPathwaysMultiple
            (fromJust rData)
            (fromJust rMart)
            (MSigDBType queryType)
convertMultiple opts@(Annotation {}) rMart rData                  =
    fmap (fmap (fromMaybe "" . fmap unAnn))
        . whichAnn (read . unHelpful . database $ opts)
        . fmap UnknownAnn
  where
    whichAnn Ensembl           = mapM toEnsemblAnn
    whichAnn (HUGO queryType)  = mapM (toHUGOAnn . HUGOType $ queryType)
    whichAnn UniProt           = mapM toUniProtAnn
    whichAnn (RGene queryType) =
        toRGeneAnnMultiple (fromJust rMart) (RType queryType)
    whichAnn (MSigDBRData _)   =
        error "MSigDBRData annotation not yet supported."

main :: IO ()
main = do
    opts <- getRecord "convert-annotation, Gregory W. Schwartz.\
                      \ Converts an unknown annotation to some other\
                      \ annotation."

    let delim = case unHelpful . delimiter $ opts of
                    Nothing         -> ','
                    (Just "\\t")    -> '\t'
                    (Just [x])      -> x
                    (Just [])       -> error "No delimiter set"
                    _         -> error "Delimiter is one character"
        csvOpts = CSV.defaultDecodeOptions
                    { CSV.decDelimiter = fromIntegral (ord delim) }

    R.withEmbeddedR R.defaultConfig $ R.runRegion $ do
        rMart <- case read . unHelpful . database $ opts of
                    (RGene _)       -> fmap Just getRMart
                    (MSigDBRData _) -> fmap Just getRMart
                    _               -> return Nothing
        rData <- case read . unHelpful . database $ opts of
                    (MSigDBRData (!file, !object, _)) ->
                        fmap Just . getRData (File file) $ object
                    _                                 -> return Nothing

        if unHelpful . strict $ opts
            then do
                contents <- liftIO B.getContents
                liftIO
                    . strictConvert opts rMart rData
                    . V.toList
                    . either error id
                    $ ( CSV.decode NoHeader contents
                     :: Either String (V.Vector [T.Text])
                      )
            else
                liftIO $ runEffect $ decodeWith csvOpts NoHeader PB.stdin
                    >-> P.concat
                    >-> (pipeConvert opts rMart rData)
                    >-> encode
                    >-> PB.stdout

        return ()