packages feed

Konf-0.1.0.0: Src/Text/Konf.hs

--------------------------------------------------------------------------------
-- |
-- Module      : Text.Konf
-- Copyright   : (C) 2013 Göktuğ Kayaalp
-- License     : Two Clause BSD License (see file LICENSE)
--
-- Maintainer  : Göktuğ Kayaalp <self@gkayaalp.com>
-- Stability   : Experimental
-- Portability : Not known
--------------------------------------------------------------------------------

{-# LANGUAGE DeriveDataTypeable #-}

-- |
-- @Text.Konf@ is an implementation of the Konf language.
module Text.Konf
( Konf
, KonfValue(..)
, KonfException(..)
, konf
, konfFile
, lookup ) where

import Prelude hiding (lookup)
import Text.ParserCombinators.Parsec
import Text.Parsec.Error
import qualified Control.Exception as CE
import qualified Control.Monad as CM
import qualified Data.Map.Lazy as DML
import qualified Data.Typeable as DT

-- |
-- This data type enumerates the possible types a returned value may be.
-- Any value will be returned as a @KonfValue value@.
data KonfValue = KonfString String
               | KonfNumber Float
               | KonfComplex Float Float
               | KonfBoolean Bool
               | KonfIdentifier String
               | KonfList [KonfValue]
               | KonfUndefined
               deriving Show

-- |
-- The @Konf@ data type, a map from strings to values. This is actually a
-- parameterized @Data.Map.Lazy.Map@ and it is possible to use method in that
-- module on this type.
type Konf = DML.Map String KonfValue

data KonfException = SyntaxError String
                     deriving (Show, DT.Typeable)

instance CE.Exception KonfException

parseIdentifier :: Parser String
parseIdentifier = letter >>= \initial -> 
                    many alphaNum >>= \rest -> return (initial : rest)

parseFloat :: Parser KonfValue
parseFloat = many1 digit >>= \d ->
               -- `optionMaybe p' tries `p', if fails w/o consuming input,
               -- returns `Nothing'.  If a number lacks a decimal part (this
               -- parser return `Nothing'), it is appended `.0', thus
               -- combining the parsing of numbers in one parser and making it
               -- possible the complex number literal include integers w/o
               -- modifying it.
               optionMaybe decimalPart >>= \f ->
                 return $ KonfNumber $ read
                   (case f of
                     Just dp -> (d ++ "." ++ dp)
                     Nothing -> (d ++ ".0"))
        -- Parse `.' here to let the parser fail w/o consuming input if the
        -- literal has no decimal part.
  where decimalPart = char '.' >> many1 digit --  >>= return

parseComplex :: Parser KonfValue
parseComplex = parseFloat >>= \ (KonfNumber r) ->
                 skip >> char '+' >> skip >> parseFloat >>= \ (KonfNumber i) ->
                   (char 'i' <|> char 'j') >> return (KonfComplex r i)
  where skip = skipMany $ oneOf " \t"

parseNumber :: Parser KonfValue
parseNumber = try parseComplex <|> try parseFloat
               
parseBoolean :: Parser KonfValue
parseBoolean = true <|> false
  where
    true  = string "true"  >> return (KonfBoolean True)
    false = string "false" >> return (KonfBoolean False)

parseString :: Parser KonfValue
parseString = begin >>= \open ->
                many (noneOf "\\\"'" <|> escape) >>= \str ->
                  end open >> (return . KonfString) str
  where begin = oneOf "\"'"
        end character = char character
        escapeChar 'n' = '\n'
        escapeChar 't' = '\t'
        escapeChar 'r' = '\r'
        escapeChar 'b' = '\b'
        escapeChar  e  =  e
        escape = char '\\' >> letter >>= return . escapeChar

parseIdentifierRValue :: Parser KonfValue
parseIdentifierRValue = return . KonfIdentifier =<< parseIdentifier

parseList :: Parser KonfValue
parseList = open >> sepBy value comma >>= \l ->
              close >> return (KonfList l)
  where skip = skipMany $ oneOf " \t\n"
        open = char '(' >> skip >> optional comment >> skip
        close = skip >> optional comment >> skip >> char ')'
        value = parseValue >>= \v ->
                  skip >> optional comment >> skip >> return v
        comma = char ',' >> skip >> optional comment >> skip

comment :: Parser ()
comment = CM.void (char '#' >> manyTill anyChar newline)

parseValue :: Parser KonfValue
parseValue = parseList <|> parseString <|> parseNumber <|> try parseBoolean <|> parseIdentifierRValue

parsePair :: Parser (String, KonfValue)
parsePair = identifier >>= \key -> 
              equals >> parseValue >>= \value ->
                skip >> return (key, value)
  where skip = many $ oneOf " \t"
        equals = skip >> char '=' >> skip
        identifier = skip >> parseIdentifier

parsePairs :: Parser [(String, KonfValue)]
parsePairs = many getPair
  where skip = comment <|> CM.void newline
        terminate = (comment <|> CM.void (many1 newline)) >> optional eof
        getPair = many skip >> parsePair >>= \pair ->
                    terminate >> return pair

showErr :: ParseError -> String
showErr = show

-- |
-- Parse the string and return the resulting @Konf@ object, that can be
-- looked up for keys by using the @lookup@ function.
konf :: String -- ^ A string in the language of Konf.
     -> Konf   -- ^ A specialized map, as the result of parsing.
konf file = DML.fromList pairs
  -- HACK: I bet appending a newline to the end of the string is the silliest hack ever.
  where pairs = case parse parsePairs "konf" (file ++ "\n") of
                  -- TODO: Don't `show' here, build the string by the error resources of parsec.
                  Left err -> CE.throw $ SyntaxError ("Syntax error: " ++ showErr err)
                  Right kvlist -> kvlist

-- |
-- Read a file and parse; then return the result as a @Konf@ object in the IO monad.
konfFile :: FilePath -> IO Konf
konfFile f = do str <- readFile f
                return $ konf str


-- |
-- Lookup keys in the Konf object.
--
-- This is actually @Data.Map.Lazy.lookup@, but it additionally handles
-- mapped to other identifiers, by recursively looking those keys up
-- until it finds a value.
lookup :: Konf -> String -> KonfValue
lookup k key = case DML.lookup key k of
                 Just (KonfIdentifier i) -> lookup k i
                 Just value              -> value
                 Nothing                 -> KonfUndefined